aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.codeclimate.yml24
-rw-r--r--.github/issue_template.md2
-rw-r--r--.github/pull_request_template.md2
-rw-r--r--.gitignore29
-rw-r--r--.rubocop.yml52
-rw-r--r--.travis.yml50
-rw-r--r--Brewfile15
-rw-r--r--CODE_OF_CONDUCT.md2
-rw-r--r--Gemfile46
-rw-r--r--Gemfile.lock395
-rw-r--r--MIT-LICENSE2
-rw-r--r--RAILS_VERSION2
-rw-r--r--README.md47
-rw-r--r--RELEASING_RAILS.md6
-rw-r--r--actioncable/.gitignore4
-rw-r--r--actioncable/CHANGELOG.md26
-rw-r--r--actioncable/MIT-LICENSE2
-rw-r--r--actioncable/README.md4
-rw-r--r--actioncable/actioncable.gemspec4
-rw-r--r--actioncable/lib/action_cable.rb4
-rw-r--r--actioncable/lib/action_cable/channel/broadcasting.rb2
-rw-r--r--actioncable/lib/action_cable/channel/callbacks.rb2
-rw-r--r--actioncable/lib/action_cable/channel/naming.rb2
-rw-r--r--actioncable/lib/action_cable/connection/base.rb5
-rw-r--r--actioncable/lib/action_cable/connection/client_socket.rb4
-rw-r--r--actioncable/lib/action_cable/connection/identification.rb2
-rw-r--r--actioncable/lib/action_cable/connection/message_buffer.rb5
-rw-r--r--actioncable/lib/action_cable/connection/subscriptions.rb8
-rw-r--r--actioncable/lib/action_cable/connection/web_socket.rb4
-rw-r--r--actioncable/lib/action_cable/engine.rb2
-rw-r--r--actioncable/lib/action_cable/gem_version.rb4
-rw-r--r--actioncable/lib/action_cable/server/configuration.rb19
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/async.rb2
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/evented_redis.rb89
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/postgresql.rb2
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/redis.rb4
-rw-r--r--actioncable/lib/rails/generators/channel/channel_generator.rb2
-rw-r--r--actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb.tt (renamed from actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb)0
-rw-r--r--actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb.tt (renamed from actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb)0
-rw-r--r--actioncable/lib/rails/generators/channel/templates/assets/cable.js.tt (renamed from actioncable/lib/rails/generators/channel/templates/assets/cable.js)0
-rw-r--r--actioncable/lib/rails/generators/channel/templates/assets/channel.coffee.tt (renamed from actioncable/lib/rails/generators/channel/templates/assets/channel.coffee)0
-rw-r--r--actioncable/lib/rails/generators/channel/templates/assets/channel.js.tt (renamed from actioncable/lib/rails/generators/channel/templates/assets/channel.js)0
-rw-r--r--actioncable/lib/rails/generators/channel/templates/channel.rb.tt (renamed from actioncable/lib/rails/generators/channel/templates/channel.rb)0
-rw-r--r--actioncable/package.json2
-rw-r--r--actioncable/test/channel/base_test.rb6
-rw-r--r--actioncable/test/channel/stream_test.rb28
-rw-r--r--actioncable/test/client_test.rb12
-rw-r--r--actioncable/test/connection/authorization_test.rb9
-rw-r--r--actioncable/test/connection/base_test.rb32
-rw-r--r--actioncable/test/connection/client_socket_test.rb20
-rw-r--r--actioncable/test/connection/identifier_test.rb13
-rw-r--r--actioncable/test/connection/stream_test.rb8
-rw-r--r--actioncable/test/connection/subscriptions_test.rb24
-rw-r--r--actioncable/test/server/base_test.rb18
-rw-r--r--actioncable/test/subscription_adapter/common.rb2
-rw-r--r--actioncable/test/subscription_adapter/evented_redis_test.rb61
-rw-r--r--actioncable/test/subscription_adapter/redis_test.rb23
-rw-r--r--actioncable/test/test_helper.rb2
-rw-r--r--actionmailer/CHANGELOG.md12
-rw-r--r--actionmailer/MIT-LICENSE2
-rw-r--r--actionmailer/README.rdoc4
-rw-r--r--actionmailer/actionmailer.gemspec2
-rw-r--r--actionmailer/lib/action_mailer.rb4
-rw-r--r--actionmailer/lib/action_mailer/base.rb18
-rw-r--r--actionmailer/lib/action_mailer/gem_version.rb4
-rw-r--r--actionmailer/lib/action_mailer/inline_preview_interceptor.rb2
-rw-r--r--actionmailer/lib/action_mailer/message_delivery.rb24
-rw-r--r--actionmailer/lib/action_mailer/preview.rb2
-rw-r--r--actionmailer/lib/action_mailer/test_helper.rb48
-rw-r--r--actionmailer/lib/rails/generators/mailer/mailer_generator.rb2
-rw-r--r--actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb.tt (renamed from actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb)0
-rw-r--r--actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt (renamed from actionmailer/lib/rails/generators/mailer/templates/mailer.rb)0
-rw-r--r--actionmailer/test/base_test.rb44
-rw-r--r--actionmailer/test/caching_test.rb26
-rw-r--r--actionmailer/test/mailers/proc_mailer.rb9
-rw-r--r--actionmailer/test/message_delivery_test.rb2
-rw-r--r--actionmailer/test/test_helper_test.rb66
-rw-r--r--actionmailer/test/url_test.rb2
-rw-r--r--actionpack/CHANGELOG.md102
-rw-r--r--actionpack/MIT-LICENSE2
-rw-r--r--actionpack/README.rdoc6
-rw-r--r--actionpack/Rakefile2
-rw-r--r--actionpack/actionpack.gemspec2
-rw-r--r--actionpack/lib/abstract_controller/base.rb4
-rw-r--r--actionpack/lib/abstract_controller/callbacks.rb12
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb25
-rw-r--r--actionpack/lib/action_controller.rb6
-rw-r--r--actionpack/lib/action_controller/api.rb3
-rw-r--r--actionpack/lib/action_controller/base.rb12
-rw-r--r--actionpack/lib/action_controller/metal.rb18
-rw-r--r--actionpack/lib/action_controller/metal/content_security_policy.rb52
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb2
-rw-r--r--actionpack/lib/action_controller/metal/default_headers.rb17
-rw-r--r--actionpack/lib/action_controller/metal/exceptions.rb8
-rw-r--r--actionpack/lib/action_controller/metal/force_ssl.rb69
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb13
-rw-r--r--actionpack/lib/action_controller/metal/implicit_render.rb14
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb8
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb8
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb25
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb23
-rw-r--r--actionpack/lib/action_controller/metal/streaming.rb2
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb30
-rw-r--r--actionpack/lib/action_controller/metal/testing.rb6
-rw-r--r--actionpack/lib/action_controller/railtie.rb2
-rw-r--r--actionpack/lib/action_controller/test_case.rb13
-rw-r--r--actionpack/lib/action_dispatch.rb3
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/content_security_policy.rb270
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb8
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb3
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/mime_types.rb13
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb5
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb32
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey.rb10
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/builder.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/transition_table.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/builder.rb4
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/dot.rb20
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/transition_table.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/nodes/node.rb6
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.y2
-rw-r--r--actionpack/lib/action_dispatch/journey/parser_extras.rb4
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb4
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb14
-rw-r--r--actionpack/lib/action_dispatch/journey/scanner.rb15
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb200
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb30
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb11
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb78
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb21
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb13
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb19
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb3
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb11
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb9
-rw-r--r--actionpack/lib/action_dispatch/routing.rb5
-rw-r--r--actionpack/lib/action_dispatch/routing/endpoint.rb9
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb151
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb45
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb3
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb4
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb38
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb22
-rw-r--r--actionpack/lib/action_dispatch/system_test_case.rb26
-rw-r--r--actionpack/lib/action_dispatch/system_testing/browser.rb49
-rw-r--r--actionpack/lib/action_dispatch/system_testing/driver.rb10
-rw-r--r--actionpack/lib/action_dispatch/system_testing/server.rb16
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb20
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb1
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb26
-rw-r--r--actionpack/lib/action_dispatch/testing/test_process.rb4
-rw-r--r--actionpack/lib/action_dispatch/testing/test_response.rb2
-rw-r--r--actionpack/lib/action_pack.rb4
-rw-r--r--actionpack/lib/action_pack/gem_version.rb4
-rw-r--r--actionpack/test/abstract/callbacks_test.rb4
-rw-r--r--actionpack/test/abstract_unit.rb14
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb36
-rw-r--r--actionpack/test/controller/api/conditional_get_test.rb2
-rw-r--r--actionpack/test/controller/api/force_ssl_test.rb4
-rw-r--r--actionpack/test/controller/base_test.rb4
-rw-r--r--actionpack/test/controller/caching_test.rb34
-rw-r--r--actionpack/test/controller/filters_test.rb4
-rw-r--r--actionpack/test/controller/flash_hash_test.rb10
-rw-r--r--actionpack/test/controller/flash_test.rb4
-rw-r--r--actionpack/test/controller/force_ssl_test.rb40
-rw-r--r--actionpack/test/controller/http_digest_authentication_test.rb11
-rw-r--r--actionpack/test/controller/integration_test.rb16
-rw-r--r--actionpack/test/controller/live_stream_test.rb2
-rw-r--r--actionpack/test/controller/log_subscriber_test.rb2
-rw-r--r--actionpack/test/controller/metal_test.rb8
-rw-r--r--actionpack/test/controller/mime/respond_to_test.rb4
-rw-r--r--actionpack/test/controller/new_base/base_test.rb3
-rw-r--r--actionpack/test/controller/output_escaping_test.rb2
-rw-r--r--actionpack/test/controller/parameters/accessors_test.rb116
-rw-r--r--actionpack/test/controller/parameters/always_permitted_parameters_test.rb2
-rw-r--r--actionpack/test/controller/parameters/dup_test.rb6
-rw-r--r--actionpack/test/controller/parameters/multi_parameter_attributes_test.rb2
-rw-r--r--actionpack/test/controller/parameters/mutators_test.rb37
-rw-r--r--actionpack/test/controller/parameters/nested_parameters_permit_test.rb2
-rw-r--r--actionpack/test/controller/parameters/parameters_permit_test.rb53
-rw-r--r--actionpack/test/controller/parameters/serialization_test.rb11
-rw-r--r--actionpack/test/controller/params_wrapper_test.rb16
-rw-r--r--actionpack/test/controller/redirect_test.rb21
-rw-r--r--actionpack/test/controller/render_test.rb34
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb19
-rw-r--r--actionpack/test/controller/resources_test.rb2
-rw-r--r--actionpack/test/controller/routing_test.rb10
-rw-r--r--actionpack/test/controller/runner_test.rb4
-rw-r--r--actionpack/test/controller/send_file_test.rb2
-rw-r--r--actionpack/test/controller/test_case_test.rb22
-rw-r--r--actionpack/test/controller/url_for_integration_test.rb1
-rw-r--r--actionpack/test/controller/url_for_test.rb2
-rw-r--r--actionpack/test/controller/url_rewriter_test.rb2
-rw-r--r--actionpack/test/dispatch/content_security_policy_test.rb519
-rw-r--r--actionpack/test/dispatch/cookies_test.rb289
-rw-r--r--actionpack/test/dispatch/debug_exceptions_test.rb24
-rw-r--r--actionpack/test/dispatch/executor_test.rb6
-rw-r--r--actionpack/test/dispatch/live_response_test.rb2
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb16
-rw-r--r--actionpack/test/dispatch/reloader_test.rb4
-rw-r--r--actionpack/test/dispatch/request/session_test.rb13
-rw-r--r--actionpack/test/dispatch/request_id_test.rb5
-rw-r--r--actionpack/test/dispatch/request_test.rb255
-rw-r--r--actionpack/test/dispatch/response_test.rb30
-rw-r--r--actionpack/test/dispatch/routing/inspector_test.rb96
-rw-r--r--actionpack/test/dispatch/routing_assertions_test.rb77
-rw-r--r--actionpack/test/dispatch/routing_test.rb56
-rw-r--r--actionpack/test/dispatch/session/cookie_store_test.rb125
-rw-r--r--actionpack/test/dispatch/ssl_test.rb12
-rw-r--r--actionpack/test/dispatch/static_test.rb11
-rw-r--r--actionpack/test/dispatch/system_testing/driver_test.rb19
-rw-r--r--actionpack/test/dispatch/system_testing/screenshot_helper_test.rb17
-rw-r--r--actionpack/test/dispatch/system_testing/server_test.rb21
-rw-r--r--actionpack/test/dispatch/system_testing/system_test_case_test.rb27
-rw-r--r--actionpack/test/dispatch/uploaded_file_test.rb8
-rw-r--r--actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb1
-rw-r--r--actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder5
-rw-r--r--actionpack/test/fixtures/public/foo/さようなら.html1
-rw-r--r--actionpack/test/fixtures/public/foo/さようなら.html.gzbin0 -> 67 bytes
-rw-r--r--actionpack/test/fixtures/公共/foo/さようなら.html1
-rw-r--r--actionpack/test/fixtures/公共/foo/さようなら.html.gzbin0 -> 67 bytes
-rw-r--r--actionpack/test/journey/nodes/symbol_test.rb4
-rw-r--r--actionpack/test/journey/route/definition/scanner_test.rb97
-rw-r--r--actionpack/test/journey/router_test.rb2
-rw-r--r--actionpack/test/journey/routes_test.rb6
-rw-r--r--actionpack/test/tmp/.gitignore0
-rw-r--r--actionview/.gitignore7
-rw-r--r--actionview/CHANGELOG.md72
-rw-r--r--actionview/MIT-LICENSE2
-rw-r--r--actionview/README.rdoc4
-rw-r--r--actionview/RUNNING_UJS_TESTS.rdoc9
-rw-r--r--actionview/RUNNING_UNIT_TESTS.rdoc15
-rw-r--r--actionview/Rakefile15
-rw-r--r--actionview/actionview.gemspec2
-rw-r--r--actionview/app/assets/javascripts/MIT-LICENSE2
-rw-r--r--actionview/app/assets/javascripts/README.md43
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee6
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/features/remote.coffee2
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee15
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee4
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/dom.coffee7
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/event.coffee28
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/form.coffee2
-rw-r--r--actionview/lib/action_view.rb5
-rw-r--r--actionview/lib/action_view/base.rb10
-rw-r--r--actionview/lib/action_view/context.rb11
-rw-r--r--actionview/lib/action_view/dependency_tracker.rb2
-rw-r--r--actionview/lib/action_view/digestor.rb25
-rw-r--r--actionview/lib/action_view/gem_version.rb4
-rw-r--r--actionview/lib/action_view/helpers.rb4
-rw-r--r--actionview/lib/action_view/helpers/active_model_helper.rb8
-rw-r--r--actionview/lib/action_view/helpers/asset_tag_helper.rb153
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb24
-rw-r--r--actionview/lib/action_view/helpers/csp_helper.rb24
-rw-r--r--actionview/lib/action_view/helpers/date_helper.rb33
-rw-r--r--actionview/lib/action_view/helpers/debug_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb72
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb42
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb17
-rw-r--r--actionview/lib/action_view/helpers/javascript_helper.rb13
-rw-r--r--actionview/lib/action_view/helpers/record_tag_helper.rb23
-rw-r--r--actionview/lib/action_view/helpers/tag_helper.rb11
-rw-r--r--actionview/lib/action_view/helpers/tags/base.rb18
-rw-r--r--actionview/lib/action_view/helpers/tags/check_box.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_check_boxes.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/label.rb4
-rw-r--r--actionview/lib/action_view/helpers/tags/radio_button.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/text_area.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/text_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/translator.rb7
-rw-r--r--actionview/lib/action_view/helpers/text_helper.rb12
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb15
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb32
-rw-r--r--actionview/lib/action_view/layouts.rb2
-rw-r--r--actionview/lib/action_view/lookup_context.rb2
-rw-r--r--actionview/lib/action_view/railtie.rb29
-rw-r--r--actionview/lib/action_view/record_identifier.rb2
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb2
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb2
-rw-r--r--actionview/lib/action_view/rendering.rb8
-rw-r--r--actionview/lib/action_view/template.rb6
-rw-r--r--actionview/lib/action_view/template/error.rb3
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb3
-rw-r--r--actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb11
-rw-r--r--actionview/lib/action_view/template/handlers/erb/erubis.rb83
-rw-r--r--actionview/lib/action_view/template/resolver.rb2
-rw-r--r--actionview/lib/action_view/test_case.rb4
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb2
-rw-r--r--actionview/package.json2
-rw-r--r--actionview/test/abstract_unit.rb53
-rw-r--r--actionview/test/actionpack/abstract/abstract_controller_test.rb2
-rw-r--r--actionview/test/actionpack/controller/render_test.rb17
-rw-r--r--actionview/test/active_record_unit.rb2
-rw-r--r--actionview/test/activerecord/multifetch_cache_test.rb35
-rw-r--r--actionview/test/activerecord/render_partial_with_record_identification_test.rb10
-rw-r--r--actionview/test/fixtures/digestor/comments/show.js.erb1
-rw-r--r--actionview/test/fixtures/layouts/streaming_with_locale.erb2
-rw-r--r--actionview/test/fixtures/public/.gitignore1
-rw-r--r--actionview/test/fixtures/test/streaming_with_locale.erb1
-rw-r--r--actionview/test/template/active_model_helper_test.rb74
-rw-r--r--actionview/test/template/asset_tag_helper_test.rb90
-rw-r--r--actionview/test/template/atom_feed_helper_test.rb2
-rw-r--r--actionview/test/template/capture_helper_test.rb30
-rw-r--r--actionview/test/template/date_helper_test.rb38
-rw-r--r--actionview/test/template/digestor_test.rb23
-rw-r--r--actionview/test/template/erb/deprecated_erubis_implementation_test.rb15
-rw-r--r--actionview/test/template/erb_util_test.rb12
-rw-r--r--actionview/test/template/form_helper/form_with_test.rb506
-rw-r--r--actionview/test/template/form_helper_test.rb101
-rw-r--r--actionview/test/template/form_options_helper_test.rb53
-rw-r--r--actionview/test/template/form_tag_helper_test.rb31
-rw-r--r--actionview/test/template/javascript_helper_test.rb4
-rw-r--r--actionview/test/template/log_subscriber_test.rb2
-rw-r--r--actionview/test/template/lookup_context_test.rb4
-rw-r--r--actionview/test/template/number_helper_test.rb74
-rw-r--r--actionview/test/template/output_safety_helper_test.rb12
-rw-r--r--actionview/test/template/record_tag_helper_test.rb33
-rw-r--r--actionview/test/template/sanitize_helper_test.rb6
-rw-r--r--actionview/test/template/streaming_render_test.rb21
-rw-r--r--actionview/test/template/tag_helper_test.rb16
-rw-r--r--actionview/test/template/test_case_test.rb2
-rw-r--r--actionview/test/template/text_helper_test.rb16
-rw-r--r--actionview/test/template/translation_helper_test.rb6
-rw-r--r--actionview/test/template/url_helper_test.rb14
-rw-r--r--actionview/test/tmp/.keep0
-rw-r--r--actionview/test/ujs/.gitignore1
-rw-r--r--actionview/test/ujs/public/test/call-ajax.js26
-rw-r--r--actionview/test/ujs/public/test/call-remote-callbacks.js104
-rw-r--r--actionview/test/ujs/public/test/call-remote.js19
-rw-r--r--actionview/test/ujs/public/test/data-confirm.js42
-rw-r--r--actionview/test/ujs/public/test/data-disable-with.js27
-rw-r--r--actionview/test/ujs/public/test/data-disable.js25
-rw-r--r--actionview/test/ujs/public/test/data-remote.js27
-rw-r--r--actionview/test/ujs/public/test/override.js4
-rw-r--r--actionview/test/ujs/public/test/settings.js12
-rw-r--r--actionview/test/ujs/server.rb26
-rw-r--r--actionview/test/ujs/views/layouts/application.html.erb7
-rw-r--r--actionview/test/ujs/views/tests/index.html.erb2
-rw-r--r--activejob/CHANGELOG.md26
-rw-r--r--activejob/MIT-LICENSE2
-rw-r--r--activejob/README.md10
-rw-r--r--activejob/Rakefile1
-rw-r--r--activejob/activejob.gemspec2
-rw-r--r--activejob/lib/active_job.rb5
-rw-r--r--activejob/lib/active_job/arguments.rb34
-rw-r--r--activejob/lib/active_job/base.rb23
-rw-r--r--activejob/lib/active_job/core.rb21
-rw-r--r--activejob/lib/active_job/enqueuing.rb2
-rw-r--r--activejob/lib/active_job/exceptions.rb14
-rw-r--r--activejob/lib/active_job/execution.rb2
-rw-r--r--activejob/lib/active_job/gem_version.rb4
-rw-r--r--activejob/lib/active_job/logging.rb5
-rw-r--r--activejob/lib/active_job/queue_adapter.rb30
-rw-r--r--activejob/lib/active_job/queue_adapters.rb4
-rw-r--r--activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb4
-rw-r--r--activejob/lib/active_job/queue_adapters/qu_adapter.rb46
-rw-r--r--activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb2
-rw-r--r--activejob/lib/active_job/railtie.rb13
-rw-r--r--activejob/lib/active_job/serializers.rb64
-rw-r--r--activejob/lib/active_job/serializers/date_serializer.rb21
-rw-r--r--activejob/lib/active_job/serializers/date_time_serializer.rb21
-rw-r--r--activejob/lib/active_job/serializers/duration_serializer.rb24
-rw-r--r--activejob/lib/active_job/serializers/object_serializer.rb54
-rw-r--r--activejob/lib/active_job/serializers/symbol_serializer.rb21
-rw-r--r--activejob/lib/active_job/serializers/time_serializer.rb21
-rw-r--r--activejob/lib/active_job/serializers/time_with_zone_serializer.rb21
-rw-r--r--activejob/lib/active_job/timezones.rb13
-rw-r--r--activejob/lib/active_job/translation.rb2
-rw-r--r--activejob/lib/rails/generators/job/job_generator.rb4
-rw-r--r--activejob/lib/rails/generators/job/templates/application_job.rb.tt (renamed from activejob/lib/rails/generators/job/templates/application_job.rb)0
-rw-r--r--activejob/lib/rails/generators/job/templates/job.rb.tt (renamed from activejob/lib/rails/generators/job/templates/job.rb)0
-rw-r--r--activejob/test/adapters/qu.rb5
-rw-r--r--activejob/test/cases/argument_serialization_test.rb58
-rw-r--r--activejob/test/cases/exceptions_test.rb9
-rw-r--r--activejob/test/cases/job_serialization_test.rb7
-rw-r--r--activejob/test/cases/serializers_test.rb98
-rw-r--r--activejob/test/cases/timezones_test.rb24
-rw-r--r--activejob/test/integration/queuing_test.rb25
-rw-r--r--activejob/test/jobs/retry_job.rb4
-rw-r--r--activejob/test/jobs/timezone_dependent_job.rb22
-rw-r--r--activejob/test/support/delayed_job/delayed/backend/test.rb4
-rw-r--r--activejob/test/support/integration/adapters/qu.rb40
-rw-r--r--activejob/test/support/integration/adapters/resque.rb14
-rw-r--r--activejob/test/support/integration/adapters/sidekiq.rb12
-rw-r--r--activejob/test/support/integration/dummy_app_template.rb3
-rw-r--r--activejob/test/support/integration/helper.rb1
-rw-r--r--activejob/test/support/integration/test_case_helpers.rb4
-rw-r--r--activemodel/CHANGELOG.md41
-rw-r--r--activemodel/MIT-LICENSE2
-rw-r--r--activemodel/README.rdoc4
-rw-r--r--activemodel/activemodel.gemspec2
-rw-r--r--activemodel/lib/active_model.rb7
-rw-r--r--activemodel/lib/active_model/attribute.rb (renamed from activerecord/lib/active_record/attribute.rb)18
-rw-r--r--activemodel/lib/active_model/attribute/user_provided_default.rb (renamed from activerecord/lib/active_record/attribute/user_provided_default.rb)29
-rw-r--r--activemodel/lib/active_model/attribute_assignment.rb2
-rw-r--r--activemodel/lib/active_model/attribute_mutation_tracker.rb (renamed from activerecord/lib/active_record/attribute_mutation_tracker.rb)24
-rw-r--r--activemodel/lib/active_model/attribute_set.rb (renamed from activerecord/lib/active_record/attribute_set.rb)9
-rw-r--r--activemodel/lib/active_model/attribute_set/builder.rb (renamed from activerecord/lib/active_record/attribute_set/builder.rb)54
-rw-r--r--activemodel/lib/active_model/attribute_set/yaml_encoder.rb (renamed from activerecord/lib/active_record/attribute_set/yaml_encoder.rb)9
-rw-r--r--activemodel/lib/active_model/attributes.rb111
-rw-r--r--activemodel/lib/active_model/dirty.rb179
-rw-r--r--activemodel/lib/active_model/errors.rb10
-rw-r--r--activemodel/lib/active_model/gem_version.rb4
-rw-r--r--activemodel/lib/active_model/lint.rb24
-rw-r--r--activemodel/lib/active_model/serialization.rb2
-rw-r--r--activemodel/lib/active_model/type.rb38
-rw-r--r--activemodel/lib/active_model/type/big_integer.rb2
-rw-r--r--activemodel/lib/active_model/type/binary.rb2
-rw-r--r--activemodel/lib/active_model/type/date.rb2
-rw-r--r--activemodel/lib/active_model/type/helpers.rb8
-rw-r--r--activemodel/lib/active_model/type/integer.rb7
-rw-r--r--activemodel/lib/active_model/type/registry.rb12
-rw-r--r--activemodel/lib/active_model/type/string.rb2
-rw-r--r--activemodel/lib/active_model/type/time.rb8
-rw-r--r--activemodel/lib/active_model/validations.rb6
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb7
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb24
-rw-r--r--activemodel/lib/active_model/validations/clusivity.rb2
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb2
-rw-r--r--activemodel/lib/active_model/validations/exclusion.rb2
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb4
-rw-r--r--activemodel/lib/active_model/validations/length.rb10
-rw-r--r--activemodel/lib/active_model/validations/validates.rb4
-rw-r--r--activemodel/test/cases/attribute_assignment_test.rb13
-rw-r--r--activemodel/test/cases/attribute_methods_test.rb4
-rw-r--r--activemodel/test/cases/attribute_set_test.rb (renamed from activerecord/test/cases/attribute_set_test.rb)45
-rw-r--r--activemodel/test/cases/attribute_test.rb (renamed from activerecord/test/cases/attribute_test.rb)22
-rw-r--r--activemodel/test/cases/attributes_dirty_test.rb205
-rw-r--r--activemodel/test/cases/attributes_test.rb97
-rw-r--r--activemodel/test/cases/callbacks_test.rb12
-rw-r--r--activemodel/test/cases/dirty_test.rb73
-rw-r--r--activemodel/test/cases/errors_test.rb26
-rw-r--r--activemodel/test/cases/secure_password_test.rb2
-rw-r--r--activemodel/test/cases/serialization_test.rb7
-rw-r--r--activemodel/test/cases/type/big_integer_test.rb1
-rw-r--r--activemodel/test/cases/type/binary_test.rb1
-rw-r--r--activemodel/test/cases/type/boolean_test.rb5
-rw-r--r--activemodel/test/cases/type/date_test.rb1
-rw-r--r--activemodel/test/cases/type/date_time_test.rb1
-rw-r--r--activemodel/test/cases/type/decimal_test.rb17
-rw-r--r--activemodel/test/cases/type/float_test.rb1
-rw-r--r--activemodel/test/cases/type/immutable_string_test.rb1
-rw-r--r--activemodel/test/cases/type/integer_test.rb1
-rw-r--r--activemodel/test/cases/type/registry_test.rb1
-rw-r--r--activemodel/test/cases/type/string_test.rb1
-rw-r--r--activemodel/test/cases/type/time_test.rb1
-rw-r--r--activemodel/test/cases/type/value_test.rb1
-rw-r--r--activemodel/test/cases/validations/absence_validation_test.rb18
-rw-r--r--activemodel/test/cases/validations/acceptance_validation_test.rb26
-rw-r--r--activemodel/test/cases/validations/callbacks_test.rb43
-rw-r--r--activemodel/test/cases/validations/conditional_validation_test.rb123
-rw-r--r--activemodel/test/cases/validations/confirmation_validation_test.rb35
-rw-r--r--activemodel/test/cases/validations/exclusion_validation_test.rb36
-rw-r--r--activemodel/test/cases/validations/format_validation_test.rb36
-rw-r--r--activemodel/test/cases/validations/inclusion_validation_test.rb88
-rw-r--r--activemodel/test/cases/validations/length_validation_test.rb211
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb40
-rw-r--r--activemodel/test/cases/validations/presence_validation_test.rb20
-rw-r--r--activemodel/test/cases/validations/validates_test.rb32
-rw-r--r--activemodel/test/cases/validations/with_validation_test.rb68
-rw-r--r--activemodel/test/cases/validations_test.rb56
-rw-r--r--activemodel/test/models/person.rb4
-rw-r--r--activemodel/test/models/topic.rb2
-rw-r--r--activerecord/.gitignore3
-rw-r--r--activerecord/CHANGELOG.md242
-rw-r--r--activerecord/MIT-LICENSE4
-rw-r--r--activerecord/README.rdoc4
-rw-r--r--activerecord/Rakefile6
-rw-r--r--activerecord/activerecord.gemspec4
-rwxr-xr-xactiverecord/bin/test7
-rw-r--r--activerecord/examples/.gitignore1
-rw-r--r--activerecord/lib/active_record.rb15
-rw-r--r--activerecord/lib/active_record/aggregations.rb9
-rw-r--r--activerecord/lib/active_record/association_relation.rb4
-rw-r--r--activerecord/lib/active_record/associations.rb79
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb45
-rw-r--r--activerecord/lib/active_record/associations/association.rb10
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb21
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb28
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb7
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb4
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb10
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb2
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb28
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb16
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb21
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb1
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb17
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb36
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb19
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb58
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb51
-rw-r--r--activerecord/lib/active_record/associations/preloader/belongs_to.rb10
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb16
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many.rb10
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb21
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one.rb10
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one_through.rb11
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb108
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb31
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb92
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb114
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb10
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb9
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb7
-rw-r--r--activerecord/lib/active_record/attributes.rb8
-rw-r--r--activerecord/lib/active_record/autosave_association.rb3
-rw-r--r--activerecord/lib/active_record/base.rb18
-rw-r--r--activerecord/lib/active_record/callbacks.rb28
-rw-r--r--activerecord/lib/active_record/collection_cache_key.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb122
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb80
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb45
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb134
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb77
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb183
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb43
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb250
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb81
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb24
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb106
-rw-r--r--activerecord/lib/active_record/connection_handling.rb6
-rw-r--r--activerecord/lib/active_record/core.rb44
-rw-r--r--activerecord/lib/active_record/counter_cache.rb2
-rw-r--r--activerecord/lib/active_record/database_configurations.rb63
-rw-r--r--activerecord/lib/active_record/enum.rb7
-rw-r--r--activerecord/lib/active_record/errors.rb42
-rw-r--r--activerecord/lib/active_record/explain.rb2
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb2
-rw-r--r--activerecord/lib/active_record/fixtures.rb74
-rw-r--r--activerecord/lib/active_record/gem_version.rb4
-rw-r--r--activerecord/lib/active_record/inheritance.rb65
-rw-r--r--activerecord/lib/active_record/internal_metadata.rb6
-rw-r--r--activerecord/lib/active_record/legacy_yaml_adapter.rb2
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb68
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb11
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb41
-rw-r--r--activerecord/lib/active_record/migration.rb295
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb8
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb92
-rw-r--r--activerecord/lib/active_record/model_schema.rb87
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb12
-rw-r--r--activerecord/lib/active_record/persistence.rb157
-rw-r--r--activerecord/lib/active_record/query_cache.rb24
-rw-r--r--activerecord/lib/active_record/querying.rb4
-rw-r--r--activerecord/lib/active_record/railtie.rb35
-rw-r--r--activerecord/lib/active_record/railties/collection_cache_association_loading.rb34
-rw-r--r--activerecord/lib/active_record/railties/controller_runtime.rb67
-rw-r--r--activerecord/lib/active_record/railties/databases.rake151
-rw-r--r--activerecord/lib/active_record/railties/jdbcmysql_error.rb18
-rw-r--r--activerecord/lib/active_record/reflection.rb85
-rw-r--r--activerecord/lib/active_record/relation.rb249
-rw-r--r--activerecord/lib/active_record/relation/batches.rb28
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb53
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb7
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb107
-rw-r--r--activerecord/lib/active_record/relation/merger.rb46
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb31
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb3
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb7
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/base_handler.rb3
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb3
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb17
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/range_handler.rb10
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb4
-rw-r--r--activerecord/lib/active_record/relation/query_attribute.rb21
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb109
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb5
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb18
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb6
-rw-r--r--activerecord/lib/active_record/sanitization.rb254
-rw-r--r--activerecord/lib/active_record/schema.rb2
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb43
-rw-r--r--activerecord/lib/active_record/schema_migration.rb4
-rw-r--r--activerecord/lib/active_record/scoping.rb10
-rw-r--r--activerecord/lib/active_record/scoping/default.rb4
-rw-r--r--activerecord/lib/active_record/scoping/named.rb16
-rw-r--r--activerecord/lib/active_record/statement_cache.rb3
-rw-r--r--activerecord/lib/active_record/store.rb31
-rw-r--r--activerecord/lib/active_record/table_metadata.rb16
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb86
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb47
-rw-r--r--activerecord/lib/active_record/test_databases.rb38
-rw-r--r--activerecord/lib/active_record/timestamp.rb22
-rw-r--r--activerecord/lib/active_record/transactions.rb2
-rw-r--r--activerecord/lib/active_record/translation.rb2
-rw-r--r--activerecord/lib/active_record/type.rb24
-rw-r--r--activerecord/lib/active_record/type/adapter_specific_registry.rb9
-rw-r--r--activerecord/lib/active_record/type_caster.rb4
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb7
-rw-r--r--activerecord/lib/active_record/type_caster/map.rb7
-rw-r--r--activerecord/lib/active_record/validations.rb10
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb6
-rw-r--r--activerecord/lib/arel.rb40
-rw-r--r--activerecord/lib/arel/alias_predication.rb9
-rw-r--r--activerecord/lib/arel/attributes.rb22
-rw-r--r--activerecord/lib/arel/attributes/attribute.rb37
-rw-r--r--activerecord/lib/arel/collectors/bind.rb24
-rw-r--r--activerecord/lib/arel/collectors/composite.rb32
-rw-r--r--activerecord/lib/arel/collectors/plain_string.rb20
-rw-r--r--activerecord/lib/arel/collectors/sql_string.rb24
-rw-r--r--activerecord/lib/arel/collectors/substitute_binds.rb29
-rw-r--r--activerecord/lib/arel/compatibility/wheres.rb35
-rw-r--r--activerecord/lib/arel/crud.rb42
-rw-r--r--activerecord/lib/arel/delete_manager.rb25
-rw-r--r--activerecord/lib/arel/errors.rb9
-rw-r--r--activerecord/lib/arel/expressions.rb29
-rw-r--r--activerecord/lib/arel/factory_methods.rb45
-rw-r--r--activerecord/lib/arel/insert_manager.rb49
-rw-r--r--activerecord/lib/arel/math.rb45
-rw-r--r--activerecord/lib/arel/nodes.rb67
-rw-r--r--activerecord/lib/arel/nodes/and.rb32
-rw-r--r--activerecord/lib/arel/nodes/ascending.rb23
-rw-r--r--activerecord/lib/arel/nodes/binary.rb52
-rw-r--r--activerecord/lib/arel/nodes/bind_param.rb28
-rw-r--r--activerecord/lib/arel/nodes/case.rb55
-rw-r--r--activerecord/lib/arel/nodes/casted.rb46
-rw-r--r--activerecord/lib/arel/nodes/count.rb12
-rw-r--r--activerecord/lib/arel/nodes/delete_statement.rb38
-rw-r--r--activerecord/lib/arel/nodes/descending.rb23
-rw-r--r--activerecord/lib/arel/nodes/equality.rb11
-rw-r--r--activerecord/lib/arel/nodes/extract.rb24
-rw-r--r--activerecord/lib/arel/nodes/false.rb16
-rw-r--r--activerecord/lib/arel/nodes/full_outer_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/function.rb44
-rw-r--r--activerecord/lib/arel/nodes/grouping.rb8
-rw-r--r--activerecord/lib/arel/nodes/in.rb8
-rw-r--r--activerecord/lib/arel/nodes/infix_operation.rb80
-rw-r--r--activerecord/lib/arel/nodes/inner_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/insert_statement.rb37
-rw-r--r--activerecord/lib/arel/nodes/join_source.rb20
-rw-r--r--activerecord/lib/arel/nodes/matches.rb18
-rw-r--r--activerecord/lib/arel/nodes/named_function.rb23
-rw-r--r--activerecord/lib/arel/nodes/node.rb60
-rw-r--r--activerecord/lib/arel/nodes/node_expression.rb13
-rw-r--r--activerecord/lib/arel/nodes/outer_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/over.rb15
-rw-r--r--activerecord/lib/arel/nodes/regexp.rb16
-rw-r--r--activerecord/lib/arel/nodes/right_outer_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/select_core.rb65
-rw-r--r--activerecord/lib/arel/nodes/select_statement.rb41
-rw-r--r--activerecord/lib/arel/nodes/sql_literal.rb16
-rw-r--r--activerecord/lib/arel/nodes/string_join.rb11
-rw-r--r--activerecord/lib/arel/nodes/table_alias.rb27
-rw-r--r--activerecord/lib/arel/nodes/terminal.rb16
-rw-r--r--activerecord/lib/arel/nodes/true.rb16
-rw-r--r--activerecord/lib/arel/nodes/unary.rb45
-rw-r--r--activerecord/lib/arel/nodes/unary_operation.rb20
-rw-r--r--activerecord/lib/arel/nodes/unqualified_column.rb22
-rw-r--r--activerecord/lib/arel/nodes/update_statement.rb40
-rw-r--r--activerecord/lib/arel/nodes/values.rb16
-rw-r--r--activerecord/lib/arel/nodes/values_list.rb24
-rw-r--r--activerecord/lib/arel/nodes/window.rb126
-rw-r--r--activerecord/lib/arel/nodes/with.rb11
-rw-r--r--activerecord/lib/arel/order_predications.rb13
-rw-r--r--activerecord/lib/arel/predications.rb241
-rw-r--r--activerecord/lib/arel/select_manager.rb273
-rw-r--r--activerecord/lib/arel/table.rb111
-rw-r--r--activerecord/lib/arel/tree_manager.rb38
-rw-r--r--activerecord/lib/arel/update_manager.rb59
-rw-r--r--activerecord/lib/arel/visitors.rb20
-rw-r--r--activerecord/lib/arel/visitors/depth_first.rb200
-rw-r--r--activerecord/lib/arel/visitors/dot.rb292
-rw-r--r--activerecord/lib/arel/visitors/ibm_db.rb15
-rw-r--r--activerecord/lib/arel/visitors/informix.rb55
-rw-r--r--activerecord/lib/arel/visitors/mssql.rb125
-rw-r--r--activerecord/lib/arel/visitors/mysql.rb87
-rw-r--r--activerecord/lib/arel/visitors/oracle.rb153
-rw-r--r--activerecord/lib/arel/visitors/oracle12.rb61
-rw-r--r--activerecord/lib/arel/visitors/postgresql.rb104
-rw-r--r--activerecord/lib/arel/visitors/sqlite.rb27
-rw-r--r--activerecord/lib/arel/visitors/to_sql.rb847
-rw-r--r--activerecord/lib/arel/visitors/visitor.rb42
-rw-r--r--activerecord/lib/arel/visitors/where_sql.rb23
-rw-r--r--activerecord/lib/arel/window_predications.rb9
-rw-r--r--activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt (renamed from activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb)0
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb6
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt (renamed from activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb)0
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt (renamed from activerecord/lib/rails/generators/active_record/migration/templates/migration.rb)0
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt (renamed from activerecord/lib/rails/generators/active_record/model/templates/model.rb)0
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt (renamed from activerecord/lib/rails/generators/active_record/model/templates/module.rb)0
-rw-r--r--activerecord/test/.gitignore1
-rw-r--r--activerecord/test/cases/adapter_test.rb14
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb14
-rw-r--r--activerecord/test/cases/adapters/mysql2/auto_increment_test.rb34
-rw-r--r--activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb28
-rw-r--r--activerecord/test/cases/adapters/mysql2/enum_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/explain_test.rb22
-rw-r--r--activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb10
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/table_options_test.rb75
-rw-r--r--activerecord/test/cases/adapters/mysql2/transaction_test.rb90
-rw-r--r--activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb9
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/bit_string_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/change_schema_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/citext_test.rb110
-rw-r--r--activerecord/test/cases/adapters/postgresql/composite_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/date_test.rb42
-rw-r--r--activerecord/test/cases/adapters/postgresql/domain_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/enum_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/explain_test.rb16
-rw-r--r--activerecord/test/cases/adapters/postgresql/extension_migration_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/foreign_table_test.rb109
-rw-r--r--activerecord/test/cases/adapters/postgresql/full_text_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb12
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb590
-rw-r--r--activerecord/test/cases/adapters/postgresql/infinity_test.rb39
-rw-r--r--activerecord/test/cases/adapters/postgresql/ltree_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/money_test.rb10
-rw-r--r--activerecord/test/cases/adapters/postgresql/network_test.rb12
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb30
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb77
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb64
-rw-r--r--activerecord/test/cases/adapters/postgresql/serial_test.rb42
-rw-r--r--activerecord/test/cases/adapters/postgresql/statement_pool_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/transaction_test.rb90
-rw-r--r--activerecord/test/cases/adapters/postgresql/type_lookup_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb198
-rw-r--r--activerecord/test/cases/adapters/sqlite3/explain_test.rb22
-rw-r--r--activerecord/test/cases/adapters/sqlite3/json_test.rb29
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb9
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb126
-rw-r--r--activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb2
-rw-r--r--activerecord/test/cases/aggregations_test.rb2
-rw-r--r--activerecord/test/cases/ar_schema_test.rb16
-rw-r--r--activerecord/test/cases/arel/attributes/attribute_test.rb1015
-rw-r--r--activerecord/test/cases/arel/attributes_test.rb68
-rw-r--r--activerecord/test/cases/arel/collectors/bind_test.rb40
-rw-r--r--activerecord/test/cases/arel/collectors/composite_test.rb47
-rw-r--r--activerecord/test/cases/arel/collectors/sql_string_test.rb47
-rw-r--r--activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb48
-rw-r--r--activerecord/test/cases/arel/crud_test.rb65
-rw-r--r--activerecord/test/cases/arel/delete_manager_test.rb52
-rw-r--r--activerecord/test/cases/arel/factory_methods_test.rb46
-rw-r--r--activerecord/test/cases/arel/helper.rb44
-rw-r--r--activerecord/test/cases/arel/insert_manager_test.rb244
-rw-r--r--activerecord/test/cases/arel/nodes/and_test.rb21
-rw-r--r--activerecord/test/cases/arel/nodes/as_test.rb36
-rw-r--r--activerecord/test/cases/arel/nodes/ascending_test.rb46
-rw-r--r--activerecord/test/cases/arel/nodes/bin_test.rb35
-rw-r--r--activerecord/test/cases/arel/nodes/binary_test.rb27
-rw-r--r--activerecord/test/cases/arel/nodes/bind_param_test.rb22
-rw-r--r--activerecord/test/cases/arel/nodes/case_test.rb84
-rw-r--r--activerecord/test/cases/arel/nodes/casted_test.rb18
-rw-r--r--activerecord/test/cases/arel/nodes/count_test.rb44
-rw-r--r--activerecord/test/cases/arel/nodes/delete_statement_test.rb36
-rw-r--r--activerecord/test/cases/arel/nodes/descending_test.rb46
-rw-r--r--activerecord/test/cases/arel/nodes/distinct_test.rb21
-rw-r--r--activerecord/test/cases/arel/nodes/equality_test.rb86
-rw-r--r--activerecord/test/cases/arel/nodes/extract_test.rb43
-rw-r--r--activerecord/test/cases/arel/nodes/false_test.rb21
-rw-r--r--activerecord/test/cases/arel/nodes/grouping_test.rb26
-rw-r--r--activerecord/test/cases/arel/nodes/infix_operation_test.rb42
-rw-r--r--activerecord/test/cases/arel/nodes/insert_statement_test.rb44
-rw-r--r--activerecord/test/cases/arel/nodes/named_function_test.rb48
-rw-r--r--activerecord/test/cases/arel/nodes/node_test.rb41
-rw-r--r--activerecord/test/cases/arel/nodes/not_test.rb31
-rw-r--r--activerecord/test/cases/arel/nodes/or_test.rb36
-rw-r--r--activerecord/test/cases/arel/nodes/over_test.rb69
-rw-r--r--activerecord/test/cases/arel/nodes/select_core_test.rb71
-rw-r--r--activerecord/test/cases/arel/nodes/select_statement_test.rb51
-rw-r--r--activerecord/test/cases/arel/nodes/sql_literal_test.rb75
-rw-r--r--activerecord/test/cases/arel/nodes/sum_test.rb35
-rw-r--r--activerecord/test/cases/arel/nodes/table_alias_test.rb29
-rw-r--r--activerecord/test/cases/arel/nodes/true_test.rb21
-rw-r--r--activerecord/test/cases/arel/nodes/unary_operation_test.rb41
-rw-r--r--activerecord/test/cases/arel/nodes/update_statement_test.rb60
-rw-r--r--activerecord/test/cases/arel/nodes/window_test.rb81
-rw-r--r--activerecord/test/cases/arel/nodes_test.rb34
-rw-r--r--activerecord/test/cases/arel/select_manager_test.rb1236
-rw-r--r--activerecord/test/cases/arel/support/fake_record.rb129
-rw-r--r--activerecord/test/cases/arel/table_test.rb216
-rw-r--r--activerecord/test/cases/arel/update_manager_test.rb126
-rw-r--r--activerecord/test/cases/arel/visitors/depth_first_test.rb271
-rw-r--r--activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb72
-rw-r--r--activerecord/test/cases/arel/visitors/dot_test.rb84
-rw-r--r--activerecord/test/cases/arel/visitors/ibm_db_test.rb34
-rw-r--r--activerecord/test/cases/arel/visitors/informix_test.rb59
-rw-r--r--activerecord/test/cases/arel/visitors/mssql_test.rb99
-rw-r--r--activerecord/test/cases/arel/visitors/mysql_test.rb80
-rw-r--r--activerecord/test/cases/arel/visitors/oracle12_test.rb61
-rw-r--r--activerecord/test/cases/arel/visitors/oracle_test.rb197
-rw-r--r--activerecord/test/cases/arel/visitors/postgres_test.rb281
-rw-r--r--activerecord/test/cases/arel/visitors/sqlite_test.rb32
-rw-r--r--activerecord/test/cases/arel/visitors/to_sql_test.rb654
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb79
-rw-r--r--activerecord/test/cases/associations/callbacks_test.rb16
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb4
-rw-r--r--activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb58
-rw-r--r--activerecord/test/cases/associations/eager_test.rb75
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb105
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb292
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb129
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb61
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb48
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb30
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb55
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb40
-rw-r--r--activerecord/test/cases/associations/left_outer_join_association_test.rb11
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb67
-rw-r--r--activerecord/test/cases/associations_test.rb32
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb2
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb84
-rw-r--r--activerecord/test/cases/attributes_test.rb8
-rw-r--r--activerecord/test/cases/autosave_association_test.rb370
-rw-r--r--activerecord/test/cases/base_test.rb160
-rw-r--r--activerecord/test/cases/batches_test.rb48
-rw-r--r--activerecord/test/cases/cache_key_test.rb4
-rw-r--r--activerecord/test/cases/calculations_test.rb56
-rw-r--r--activerecord/test/cases/callbacks_test.rb36
-rw-r--r--activerecord/test/cases/clone_test.rb2
-rw-r--r--activerecord/test/cases/collection_cache_key_test.rb36
-rw-r--r--activerecord/test/cases/comment_test.rb22
-rw-r--r--activerecord/test/cases/connection_adapters/adapter_leasing_test.rb4
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb142
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb8
-rw-r--r--activerecord/test/cases/connection_adapters/type_lookup_test.rb6
-rw-r--r--activerecord/test/cases/connection_management_test.rb14
-rw-r--r--activerecord/test/cases/connection_pool_test.rb86
-rw-r--r--activerecord/test/cases/connection_specification/resolver_test.rb2
-rw-r--r--activerecord/test/cases/core_test.rb81
-rw-r--r--activerecord/test/cases/date_time_precision_test.rb18
-rw-r--r--activerecord/test/cases/defaults_test.rb2
-rw-r--r--activerecord/test/cases/dirty_test.rb221
-rw-r--r--activerecord/test/cases/dup_test.rb32
-rw-r--r--activerecord/test/cases/enum_test.rb158
-rw-r--r--activerecord/test/cases/explain_subscriber_test.rb10
-rw-r--r--activerecord/test/cases/explain_test.rb3
-rw-r--r--activerecord/test/cases/finder_respond_to_test.rb12
-rw-r--r--activerecord/test/cases/finder_test.rb144
-rw-r--r--activerecord/test/cases/fixtures_test.rb171
-rw-r--r--activerecord/test/cases/habtm_destroy_order_test.rb6
-rw-r--r--activerecord/test/cases/helper.rb2
-rw-r--r--activerecord/test/cases/inheritance_test.rb76
-rw-r--r--activerecord/test/cases/instrumentation_test.rb72
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb45
-rw-r--r--activerecord/test/cases/json_attribute_test.rb6
-rw-r--r--activerecord/test/cases/json_serialization_test.rb2
-rw-r--r--activerecord/test/cases/json_shared_test_cases.rb55
-rw-r--r--activerecord/test/cases/locking_test.rb141
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb21
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb23
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb6
-rw-r--r--activerecord/test/cases/migration/columns_test.rb14
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb9
-rw-r--r--activerecord/test/cases/migration/compatibility_test.rb159
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb14
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb79
-rw-r--r--activerecord/test/cases/migration/index_test.rb14
-rw-r--r--activerecord/test/cases/migration/pending_migrations_test.rb50
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb6
-rw-r--r--activerecord/test/cases/migration_test.rb174
-rw-r--r--activerecord/test/cases/migrator_test.rb152
-rw-r--r--activerecord/test/cases/multiparameter_attributes_test.rb8
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb40
-rw-r--r--activerecord/test/cases/nested_attributes_with_callbacks_test.rb10
-rw-r--r--activerecord/test/cases/numeric_data_test.rb14
-rw-r--r--activerecord/test/cases/persistence_test.rb324
-rw-r--r--activerecord/test/cases/primary_keys_test.rb24
-rw-r--r--activerecord/test/cases/query_cache_test.rb53
-rw-r--r--activerecord/test/cases/quoting_test.rb78
-rw-r--r--activerecord/test/cases/readonly_test.rb56
-rw-r--r--activerecord/test/cases/reaper_test.rb16
-rw-r--r--activerecord/test/cases/reflection_test.rb151
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb28
-rw-r--r--activerecord/test/cases/relation/merging_test.rb16
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb11
-rw-r--r--activerecord/test/cases/relation/or_test.rb7
-rw-r--r--activerecord/test/cases/relation/select_test.rb15
-rw-r--r--activerecord/test/cases/relation/where_clause_test.rb4
-rw-r--r--activerecord/test/cases/relation/where_test.rb2
-rw-r--r--activerecord/test/cases/relation_test.rb91
-rw-r--r--activerecord/test/cases/relations_test.rb384
-rw-r--r--activerecord/test/cases/reserved_word_test.rb11
-rw-r--r--activerecord/test/cases/sanitize_test.rb79
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb72
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb48
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb92
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb36
-rw-r--r--activerecord/test/cases/serialization_test.rb4
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb4
-rw-r--r--activerecord/test/cases/statement_cache_test.rb5
-rw-r--r--activerecord/test/cases/store_test.rb28
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb554
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb150
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb106
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb23
-rw-r--r--activerecord/test/cases/test_case.rb8
-rw-r--r--activerecord/test/cases/time_precision_test.rb18
-rw-r--r--activerecord/test/cases/timestamp_test.rb34
-rw-r--r--activerecord/test/cases/touch_later_test.rb4
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb30
-rw-r--r--activerecord/test/cases/transaction_isolation_test.rb4
-rw-r--r--activerecord/test/cases/transactions_test.rb108
-rw-r--r--activerecord/test/cases/type/string_test.rb6
-rw-r--r--activerecord/test/cases/unsafe_raw_sql_test.rb319
-rw-r--r--activerecord/test/cases/validations/absence_validation_test.rb10
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb24
-rw-r--r--activerecord/test/cases/validations/length_validation_test.rb36
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb18
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb42
-rw-r--r--activerecord/test/cases/validations_test.rb18
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb4
-rw-r--r--activerecord/test/fixtures/.gitignore1
-rw-r--r--activerecord/test/fixtures/customers.yml11
-rw-r--r--activerecord/test/fixtures/minimalistics.yml3
-rw-r--r--activerecord/test/fixtures/other_posts.yml1
-rw-r--r--activerecord/test/fixtures/posts.yml8
-rw-r--r--activerecord/test/fixtures/reserved_words/values.yml4
-rw-r--r--activerecord/test/fixtures/sponsors.yml3
-rw-r--r--activerecord/test/fixtures/teapots.yml3
-rw-r--r--activerecord/test/migrations/decimal/1_give_me_big_numbers.rb2
-rw-r--r--activerecord/test/models/admin/user.rb3
-rw-r--r--activerecord/test/models/author.rb12
-rw-r--r--activerecord/test/models/comment.rb8
-rw-r--r--activerecord/test/models/company.rb2
-rw-r--r--activerecord/test/models/contract.rb2
-rw-r--r--activerecord/test/models/customer.rb2
-rw-r--r--activerecord/test/models/developer.rb11
-rw-r--r--activerecord/test/models/drink_designer.rb3
-rw-r--r--activerecord/test/models/face.rb5
-rw-r--r--activerecord/test/models/frog.rb8
-rw-r--r--activerecord/test/models/man.rb3
-rw-r--r--activerecord/test/models/member_detail.rb1
-rw-r--r--activerecord/test/models/mocktail_designer.rb4
-rw-r--r--activerecord/test/models/post.rb18
-rw-r--r--activerecord/test/models/sponsor.rb1
-rw-r--r--activerecord/test/models/tag.rb3
-rw-r--r--activerecord/test/models/tagging.rb5
-rw-r--r--activerecord/test/models/topic.rb15
-rw-r--r--activerecord/test/models/wheel.rb2
-rw-r--r--activerecord/test/schema/schema.rb23
-rw-r--r--activestorage/.babelrc3
-rw-r--r--activestorage/.gitignore12
-rw-r--r--activestorage/CHANGELOG.md26
-rw-r--r--activestorage/MIT-LICENSE2
-rw-r--r--activestorage/README.md24
-rw-r--r--activestorage/Rakefile3
-rw-r--r--activestorage/activestorage.gemspec6
-rw-r--r--activestorage/app/assets/javascripts/activestorage.js931
-rw-r--r--activestorage/app/controllers/active_storage/base_controller.rb10
-rw-r--r--activestorage/app/controllers/active_storage/blobs_controller.rb21
-rw-r--r--activestorage/app/controllers/active_storage/direct_uploads_controller.rb4
-rw-r--r--activestorage/app/controllers/active_storage/disk_controller.rb10
-rw-r--r--activestorage/app/controllers/active_storage/representations_controller.rb14
-rw-r--r--activestorage/app/controllers/active_storage/variants_controller.rb29
-rw-r--r--activestorage/app/controllers/concerns/active_storage/set_blob.rb16
-rw-r--r--activestorage/app/javascript/activestorage/blob_record.js20
-rw-r--r--activestorage/app/javascript/activestorage/blob_upload.js2
-rw-r--r--activestorage/app/javascript/activestorage/direct_upload.js6
-rw-r--r--activestorage/app/javascript/activestorage/helpers.js11
-rw-r--r--activestorage/app/javascript/activestorage/ujs.js2
-rw-r--r--activestorage/app/jobs/active_storage/analyze_job.rb8
-rw-r--r--activestorage/app/jobs/active_storage/base_job.rb5
-rw-r--r--activestorage/app/jobs/active_storage/purge_job.rb4
-rw-r--r--activestorage/app/models/active_storage/attachment.rb11
-rw-r--r--activestorage/app/models/active_storage/blob.rb64
-rw-r--r--activestorage/app/models/active_storage/blob/analyzable.rb57
-rw-r--r--activestorage/app/models/active_storage/blob/identifiable.rb20
-rw-r--r--activestorage/app/models/active_storage/blob/representable.rb93
-rw-r--r--activestorage/app/models/active_storage/current.rb5
-rw-r--r--activestorage/app/models/active_storage/filename.rb32
-rw-r--r--activestorage/app/models/active_storage/filename/parameters.rb2
-rw-r--r--activestorage/app/models/active_storage/preview.rb89
-rw-r--r--activestorage/app/models/active_storage/variant.rb99
-rw-r--r--activestorage/app/models/active_storage/variation.rb96
-rw-r--r--activestorage/config/routes.rb31
-rw-r--r--activestorage/lib/active_storage.rb18
-rw-r--r--activestorage/lib/active_storage/analyzer.rb33
-rw-r--r--activestorage/lib/active_storage/analyzer/image_analyzer.rb45
-rw-r--r--activestorage/lib/active_storage/analyzer/null_analyzer.rb13
-rw-r--r--activestorage/lib/active_storage/analyzer/video_analyzer.rb118
-rw-r--r--activestorage/lib/active_storage/attached/macros.rb36
-rw-r--r--activestorage/lib/active_storage/attached/many.rb30
-rw-r--r--activestorage/lib/active_storage/attached/one.rb40
-rw-r--r--activestorage/lib/active_storage/downloading.rb39
-rw-r--r--activestorage/lib/active_storage/engine.rb46
-rw-r--r--activestorage/lib/active_storage/errors.rb7
-rw-r--r--activestorage/lib/active_storage/gem_version.rb4
-rw-r--r--activestorage/lib/active_storage/log_subscriber.rb6
-rw-r--r--activestorage/lib/active_storage/previewer.rb74
-rw-r--r--activestorage/lib/active_storage/previewer/mupdf_previewer.rb36
-rw-r--r--activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb35
-rw-r--r--activestorage/lib/active_storage/previewer/video_previewer.rb27
-rw-r--r--activestorage/lib/active_storage/service.rb23
-rw-r--r--activestorage/lib/active_storage/service/azure_storage_service.rb71
-rw-r--r--activestorage/lib/active_storage/service/disk_service.rb60
-rw-r--r--activestorage/lib/active_storage/service/gcs_service.rb79
-rw-r--r--activestorage/lib/active_storage/service/mirror_service.rb7
-rw-r--r--activestorage/lib/active_storage/service/s3_service.rb40
-rw-r--r--activestorage/lib/tasks/activestorage.rake6
-rw-r--r--activestorage/package.json22
-rw-r--r--activestorage/rollup.config.js28
-rw-r--r--activestorage/test/analyzer/image_analyzer_test.rb32
-rw-r--r--activestorage/test/analyzer/video_analyzer_test.rb54
-rw-r--r--activestorage/test/controllers/blobs_controller_test.rb7
-rw-r--r--activestorage/test/controllers/direct_uploads_controller_test.rb27
-rw-r--r--activestorage/test/controllers/representations_controller_test.rb61
-rw-r--r--activestorage/test/controllers/variants_controller_test.rb23
-rw-r--r--activestorage/test/database/setup.rb2
-rwxr-xr-xactivestorage/test/dummy/bin/bundle2
-rw-r--r--activestorage/test/dummy/config/application.rb6
-rw-r--r--activestorage/test/dummy/config/environments/test.rb3
-rw-r--r--activestorage/test/fixtures/files/favicon.icobin0 -> 16958 bytes
-rw-r--r--activestorage/test/fixtures/files/icon.psdbin0 -> 37441 bytes
-rw-r--r--activestorage/test/fixtures/files/icon.svg13
-rw-r--r--activestorage/test/fixtures/files/image.gifbin0 -> 2032 bytes
-rw-r--r--activestorage/test/fixtures/files/racecar_rotated.jpgbin0 -> 1124060 bytes
-rw-r--r--activestorage/test/fixtures/files/report.pdfbin0 -> 13469 bytes
-rw-r--r--activestorage/test/fixtures/files/rotated_video.mp4bin0 -> 275090 bytes
-rw-r--r--activestorage/test/fixtures/files/video.mp4bin0 -> 275433 bytes
-rw-r--r--activestorage/test/fixtures/files/video_with_rectangular_samples.mp4bin0 -> 361535 bytes
-rw-r--r--activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4bin0 -> 128737 bytes
-rw-r--r--activestorage/test/fixtures/files/video_without_video_stream.mp4bin0 -> 16252 bytes
-rw-r--r--activestorage/test/models/attachments_test.rb321
-rw-r--r--activestorage/test/models/blob_test.rb101
-rw-r--r--activestorage/test/models/presence_validation_test.rb30
-rw-r--r--activestorage/test/models/preview_test.rb40
-rw-r--r--activestorage/test/models/representation_test.rb41
-rw-r--r--activestorage/test/models/variant_test.rb107
-rw-r--r--activestorage/test/previewer/mupdf_previewer_test.rb23
-rw-r--r--activestorage/test/previewer/poppler_pdf_previewer_test.rb23
-rw-r--r--activestorage/test/previewer/video_previewer_test.rb23
-rw-r--r--activestorage/test/service/azure_storage_service_test.rb4
-rw-r--r--activestorage/test/service/configurations.example.yml3
-rw-r--r--activestorage/test/service/configurations.yml.encbin2864 -> 2848 bytes
-rw-r--r--activestorage/test/service/configurator_test.rb1
-rw-r--r--activestorage/test/service/disk_service_test.rb4
-rw-r--r--activestorage/test/service/gcs_service_test.rb21
-rw-r--r--activestorage/test/service/mirror_service_test.rb40
-rw-r--r--activestorage/test/service/s3_service_test.rb6
-rw-r--r--activestorage/test/service/shared_service_tests.rb32
-rw-r--r--activestorage/test/template/image_tag_test.rb10
-rw-r--r--activestorage/test/test_helper.rb45
-rw-r--r--activestorage/webpack.config.js27
-rw-r--r--activestorage/yarn.lock1491
-rw-r--r--activesupport/.gitignore1
-rw-r--r--activesupport/CHANGELOG.md242
-rw-r--r--activesupport/MIT-LICENSE2
-rw-r--r--activesupport/README.rdoc2
-rw-r--r--activesupport/Rakefile20
-rw-r--r--activesupport/activesupport.gemspec4
-rwxr-xr-xactivesupport/bin/generate_tables141
-rw-r--r--activesupport/lib/active_support.rb25
-rw-r--r--activesupport/lib/active_support/all.rb4
-rw-r--r--activesupport/lib/active_support/benchmarkable.rb4
-rw-r--r--activesupport/lib/active_support/cache.rb220
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb13
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb70
-rw-r--r--activesupport/lib/active_support/cache/redis_cache_store.rb460
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb30
-rw-r--r--activesupport/lib/active_support/callbacks.rb79
-rw-r--r--activesupport/lib/active_support/concern.rb2
-rw-r--r--activesupport/lib/active_support/concurrency/load_interlock_aware_monitor.rb17
-rw-r--r--activesupport/lib/active_support/configurable.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/array.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/array/access.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/array/conversions.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/array/inquiry.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/big_decimal.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/class.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute_accessors.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/class/subclasses.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/date.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/date/acts_like.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date/calculations.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/date/conversions.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/date/zones.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/calculations.rb68
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date_time.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/acts_like.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/compatibility.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/conversions.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/enumerable.rb107
-rw-r--r--activesupport/lib/active_support/core_ext/file.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/hash/compact.rb28
-rw-r--r--activesupport/lib/active_support/core_ext/hash/conversions.rb22
-rw-r--r--activesupport/lib/active_support/core_ext/hash/indifferent_access.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash/slice.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/hash/transform_values.rb31
-rw-r--r--activesupport/lib/active_support/core_ext/integer.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/integer/inflections.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/integer/time.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/kernel.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/concern.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/module.rb24
-rw-r--r--activesupport/lib/active_support/core_ext/module/attribute_accessors.rb8
-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/concerning.rb15
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb36
-rw-r--r--activesupport/lib/active_support/core_ext/module/introspection.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/module/reachable.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/module/redefine_method.rb25
-rw-r--r--activesupport/lib/active_support/core_ext/module/remove_method.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/name_error.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/numeric.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/conversions.rb28
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/inquiry.rb27
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/time.rb30
-rw-r--r--activesupport/lib/active_support/core_ext/object.rb24
-rw-r--r--activesupport/lib/active_support/core_ext/object/acts_like.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/object/blank.rb13
-rw-r--r--activesupport/lib/active_support/core_ext/object/conversions.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/object/deep_dup.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/object/duplicable.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/object/json.rb21
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_param.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/object/with_options.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/range.rb9
-rw-r--r--activesupport/lib/active_support/core_ext/range/conversions.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/range/each.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/range/include_time_with_zone.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/regexp.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/string.rb26
-rw-r--r--activesupport/lib/active_support/core_ext/string/conversions.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/string/filters.rb41
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/string/inquiry.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/string/multibyte.rb9
-rw-r--r--activesupport/lib/active_support/core_ext/string/output_safety.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/string/strip.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/string/zones.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/time.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/time/acts_like.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/time/calculations.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/time/compatibility.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/time/conversions.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/time/zones.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/uri.rb6
-rw-r--r--activesupport/lib/active_support/dependencies.rb27
-rw-r--r--activesupport/lib/active_support/dependencies/autoload.rb2
-rw-r--r--activesupport/lib/active_support/dependencies/interlock.rb2
-rw-r--r--activesupport/lib/active_support/deprecation.rb16
-rw-r--r--activesupport/lib/active_support/deprecation/behaviors.rb6
-rw-r--r--activesupport/lib/active_support/deprecation/constant_accessor.rb6
-rw-r--r--activesupport/lib/active_support/deprecation/instance_delegator.rb4
-rw-r--r--activesupport/lib/active_support/deprecation/method_wrappers.rb25
-rw-r--r--activesupport/lib/active_support/deprecation/proxy_wrappers.rb7
-rw-r--r--activesupport/lib/active_support/deprecation/reporting.rb4
-rw-r--r--activesupport/lib/active_support/digest.rb20
-rw-r--r--activesupport/lib/active_support/duration.rb22
-rw-r--r--activesupport/lib/active_support/duration/iso8601_parser.rb4
-rw-r--r--activesupport/lib/active_support/duration/iso8601_serializer.rb3
-rw-r--r--activesupport/lib/active_support/encrypted_configuration.rb17
-rw-r--r--activesupport/lib/active_support/encrypted_file.rb14
-rw-r--r--activesupport/lib/active_support/execution_wrapper.rb2
-rw-r--r--activesupport/lib/active_support/executor.rb2
-rw-r--r--activesupport/lib/active_support/file_update_checker.rb2
-rw-r--r--activesupport/lib/active_support/gem_version.rb4
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb55
-rw-r--r--activesupport/lib/active_support/i18n.rb9
-rw-r--r--activesupport/lib/active_support/i18n_railtie.rb4
-rw-r--r--activesupport/lib/active_support/inflections.rb2
-rw-r--r--activesupport/lib/active_support/inflector.rb10
-rw-r--r--activesupport/lib/active_support/inflector/inflections.rb28
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb25
-rw-r--r--activesupport/lib/active_support/inflector/transliterate.rb20
-rw-r--r--activesupport/lib/active_support/json.rb4
-rw-r--r--activesupport/lib/active_support/json/decoding.rb4
-rw-r--r--activesupport/lib/active_support/json/encoding.rb12
-rw-r--r--activesupport/lib/active_support/locale/en.rb28
-rw-r--r--activesupport/lib/active_support/log_subscriber.rb6
-rw-r--r--activesupport/lib/active_support/log_subscriber/test_helper.rb6
-rw-r--r--activesupport/lib/active_support/logger.rb4
-rw-r--r--activesupport/lib/active_support/logger_silence.rb4
-rw-r--r--activesupport/lib/active_support/logger_thread_safe_level.rb2
-rw-r--r--activesupport/lib/active_support/message_encryptor.rb39
-rw-r--r--activesupport/lib/active_support/message_verifier.rb41
-rw-r--r--activesupport/lib/active_support/messages/rotation_configuration.rb22
-rw-r--r--activesupport/lib/active_support/messages/rotator.rb56
-rw-r--r--activesupport/lib/active_support/multibyte/chars.rb10
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb289
-rw-r--r--activesupport/lib/active_support/notifications.rb6
-rw-r--r--activesupport/lib/active_support/number_helper/number_converter.rb10
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_currency_converter.rb2
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb20
-rw-r--r--activesupport/lib/active_support/number_helper/rounding_helper.rb2
-rw-r--r--activesupport/lib/active_support/option_merger.rb2
-rw-r--r--activesupport/lib/active_support/ordered_options.rb6
-rw-r--r--activesupport/lib/active_support/per_thread_registry.rb2
-rw-r--r--activesupport/lib/active_support/rails.rb18
-rw-r--r--activesupport/lib/active_support/railtie.rb22
-rw-r--r--activesupport/lib/active_support/reloader.rb5
-rw-r--r--activesupport/lib/active_support/rescuable.rb6
-rw-r--r--activesupport/lib/active_support/security_utils.rb24
-rw-r--r--activesupport/lib/active_support/subscriber.rb27
-rw-r--r--activesupport/lib/active_support/tagged_logging.rb10
-rw-r--r--activesupport/lib/active_support/test_case.rb107
-rw-r--r--activesupport/lib/active_support/testing/assertions.rb41
-rw-r--r--activesupport/lib/active_support/testing/constant_lookup.rb4
-rw-r--r--activesupport/lib/active_support/testing/deprecation.rb4
-rw-r--r--activesupport/lib/active_support/testing/isolation.rb10
-rw-r--r--activesupport/lib/active_support/testing/parallelization.rb102
-rw-r--r--activesupport/lib/active_support/testing/setup_and_teardown.rb19
-rw-r--r--activesupport/lib/active_support/testing/stream.rb2
-rw-r--r--activesupport/lib/active_support/testing/time_helpers.rb8
-rw-r--r--activesupport/lib/active_support/time.rb14
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb10
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb27
-rw-r--r--activesupport/lib/active_support/values/unicode_tables.datbin1116857 -> 0 bytes
-rw-r--r--activesupport/lib/active_support/xml_mini.rb14
-rw-r--r--activesupport/lib/active_support/xml_mini/jdom.rb2
-rw-r--r--activesupport/lib/active_support/xml_mini/libxml.rb2
-rw-r--r--activesupport/lib/active_support/xml_mini/libxmlsax.rb2
-rw-r--r--activesupport/lib/active_support/xml_mini/nokogiri.rb2
-rw-r--r--activesupport/lib/active_support/xml_mini/nokogirisax.rb2
-rw-r--r--activesupport/lib/active_support/xml_mini/rexml.rb4
-rw-r--r--activesupport/test/abstract_unit.rb4
-rw-r--r--activesupport/test/array_inquirer_test.rb6
-rw-r--r--activesupport/test/benchmarkable_test.rb2
-rw-r--r--activesupport/test/cache/behaviors.rb3
-rw-r--r--activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb4
-rw-r--r--activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb8
-rw-r--r--activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb (renamed from activesupport/test/cache/cache_store_write_multi_test.rb)42
-rw-r--r--activesupport/test/cache/behaviors/cache_store_behavior.rb176
-rw-r--r--activesupport/test/cache/behaviors/cache_store_version_behavior.rb4
-rw-r--r--activesupport/test/cache/behaviors/connection_pool_behavior.rb60
-rw-r--r--activesupport/test/cache/behaviors/failure_safety_behavior.rb91
-rw-r--r--activesupport/test/cache/behaviors/local_cache_behavior.rb28
-rw-r--r--activesupport/test/cache/cache_entry_test.rb14
-rw-r--r--activesupport/test/cache/cache_store_logger_test.rb4
-rw-r--r--activesupport/test/cache/cache_store_namespace_test.rb4
-rw-r--r--activesupport/test/cache/stores/file_store_test.rb10
-rw-r--r--activesupport/test/cache/stores/mem_cache_store_test.rb63
-rw-r--r--activesupport/test/cache/stores/memory_store_test.rb13
-rw-r--r--activesupport/test/cache/stores/redis_cache_store_test.rb242
-rw-r--r--activesupport/test/callback_inheritance_test.rb14
-rw-r--r--activesupport/test/callbacks_test.rb31
-rw-r--r--activesupport/test/class_cache_test.rb22
-rw-r--r--activesupport/test/concern_test.rb2
-rw-r--r--activesupport/test/concurrency/load_interlock_aware_monitor_test.rb55
-rw-r--r--activesupport/test/configurable_test.rb8
-rw-r--r--activesupport/test/core_ext/array/conversions_test.rb6
-rw-r--r--activesupport/test/core_ext/array/grouping_test.rb11
-rw-r--r--activesupport/test/core_ext/bigdecimal_test.rb2
-rw-r--r--activesupport/test/core_ext/class_test.rb4
-rw-r--r--activesupport/test/core_ext/date_and_time_behavior.rb130
-rw-r--r--activesupport/test/core_ext/date_ext_test.rb18
-rw-r--r--activesupport/test/core_ext/date_time_ext_test.rb66
-rw-r--r--activesupport/test/core_ext/duration_test.rb33
-rw-r--r--activesupport/test/core_ext/file_test.rb6
-rw-r--r--activesupport/test/core_ext/hash/transform_values_test.rb69
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb46
-rw-r--r--activesupport/test/core_ext/integer_ext_test.rb6
-rw-r--r--activesupport/test/core_ext/load_error_test.rb2
-rw-r--r--activesupport/test/core_ext/module/anonymous_test.rb8
-rw-r--r--activesupport/test/core_ext/module/attr_internal_test.rb12
-rw-r--r--activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb8
-rw-r--r--activesupport/test/core_ext/module/attribute_accessor_test.rb8
-rw-r--r--activesupport/test/core_ext/module/attribute_aliasing_test.rb14
-rw-r--r--activesupport/test/core_ext/module/concerning_test.rb10
-rw-r--r--activesupport/test/core_ext/module/reachable_test.rb28
-rw-r--r--activesupport/test/core_ext/module/remove_method_test.rb12
-rw-r--r--activesupport/test/core_ext/module_test.rb87
-rw-r--r--activesupport/test/core_ext/name_error_test.rb2
-rw-r--r--activesupport/test/core_ext/numeric_ext_test.rb95
-rw-r--r--activesupport/test/core_ext/object/acts_like_test.rb10
-rw-r--r--activesupport/test/core_ext/object/blank_test.rb4
-rw-r--r--activesupport/test/core_ext/object/deep_dup_test.rb2
-rw-r--r--activesupport/test/core_ext/object/duplicable_test.rb12
-rw-r--r--activesupport/test/core_ext/object/inclusion_test.rb14
-rw-r--r--activesupport/test/core_ext/object/instance_variables_test.rb4
-rw-r--r--activesupport/test/core_ext/object/try_test.rb17
-rw-r--r--activesupport/test/core_ext/range_ext_test.rb26
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb136
-rw-r--r--activesupport/test/core_ext/time_ext_test.rb38
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb109
-rw-r--r--activesupport/test/core_ext/uri_ext_test.rb2
-rw-r--r--activesupport/test/dependencies_test.rb77
-rw-r--r--activesupport/test/deprecation/method_wrappers_test.rb22
-rw-r--r--activesupport/test/deprecation/proxy_wrappers_test.rb6
-rw-r--r--activesupport/test/deprecation_test.rb2
-rw-r--r--activesupport/test/descendants_tracker_test_cases.rb2
-rw-r--r--activesupport/test/descendants_tracker_with_autoloading_test.rb2
-rw-r--r--activesupport/test/descendants_tracker_without_autoloading_test.rb2
-rw-r--r--activesupport/test/digest_test.rb27
-rw-r--r--activesupport/test/encrypted_configuration_test.rb22
-rw-r--r--activesupport/test/encrypted_file_test.rb13
-rw-r--r--activesupport/test/evented_file_update_checker_test.rb10
-rw-r--r--activesupport/test/file_update_checker_shared_tests.rb28
-rw-r--r--activesupport/test/hash_with_indifferent_access_test.rb50
-rw-r--r--activesupport/test/inflector_test.rb47
-rw-r--r--activesupport/test/inflector_test_cases.rb2
-rw-r--r--activesupport/test/json/encoding_test.rb12
-rw-r--r--activesupport/test/key_generator_test.rb6
-rw-r--r--activesupport/test/message_encryptor_test.rb70
-rw-r--r--activesupport/test/message_verifier_test.rb60
-rw-r--r--activesupport/test/messages/rotation_configuration_test.rb25
-rw-r--r--activesupport/test/multibyte_chars_test.rb14
-rw-r--r--activesupport/test/multibyte_unicode_database_test.rb26
-rw-r--r--activesupport/test/notifications/instrumenter_test.rb4
-rw-r--r--activesupport/test/notifications_test.rb10
-rw-r--r--activesupport/test/number_helper_i18n_test.rb18
-rw-r--r--activesupport/test/number_helper_test.rb14
-rw-r--r--activesupport/test/ordered_options_test.rb23
-rw-r--r--activesupport/test/reloader_test.rb10
-rw-r--r--activesupport/test/safe_buffer_test.rb14
-rw-r--r--activesupport/test/security_utils_test.rb12
-rw-r--r--activesupport/test/string_inquirer_test.rb4
-rw-r--r--activesupport/test/tagged_logging_test.rb5
-rw-r--r--activesupport/test/test_case_test.rb49
-rw-r--r--activesupport/test/testing/after_teardown_test.rb36
-rw-r--r--activesupport/test/time_travel_test.rb2
-rw-r--r--activesupport/test/time_zone_test.rb58
-rw-r--r--activesupport/test/time_zone_test_helpers.rb13
-rw-r--r--activesupport/test/xml_mini/libxml_engine_test.rb2
-rw-r--r--activesupport/test/xml_mini_test.rb2
-rwxr-xr-xci/custom_cops/bin/test5
-rw-r--r--ci/custom_cops/lib/custom_cops.rb4
-rw-r--r--ci/custom_cops/lib/custom_cops/assert_not.rb40
-rw-r--r--ci/custom_cops/lib/custom_cops/refute_not.rb71
-rw-r--r--ci/custom_cops/test/custom_cops/assert_not_test.rb42
-rw-r--r--ci/custom_cops/test/custom_cops/refute_not_test.rb66
-rw-r--r--ci/custom_cops/test/support/cop_helper.rb47
-rw-r--r--ci/phantomjs.js149
-rw-r--r--ci/qunit-selenium-runner.rb16
-rwxr-xr-xci/travis.rb4
-rw-r--r--guides/CHANGELOG.md7
-rw-r--r--guides/assets/images/4_0_release_notes/rails4_features.png (renamed from guides/assets/images/rails4_features.png)bin65840 -> 65840 bytes
-rw-r--r--guides/assets/images/akshaysurve.jpgbin3444 -> 0 bytes
-rw-r--r--guides/assets/images/association_basics/belongs_to.pngbin0 -> 35041 bytes
-rw-r--r--guides/assets/images/association_basics/habtm.pngbin0 -> 61435 bytes
-rw-r--r--guides/assets/images/association_basics/has_many.pngbin0 -> 36233 bytes
-rw-r--r--guides/assets/images/association_basics/has_many_through.pngbin0 -> 98834 bytes
-rw-r--r--guides/assets/images/association_basics/has_one.pngbin0 -> 38222 bytes
-rw-r--r--guides/assets/images/association_basics/has_one_through.pngbin0 -> 92535 bytes
-rw-r--r--guides/assets/images/association_basics/polymorphic.pngbin0 -> 84739 bytes
-rw-r--r--guides/assets/images/belongs_to.pngbin22147 -> 0 bytes
-rw-r--r--guides/assets/images/credits_pic_blank.gifbin597 -> 0 bytes
-rw-r--r--guides/assets/images/fxn.pngbin15436 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/rails_welcome.pngbin732190 -> 282547 bytes
-rw-r--r--guides/assets/images/getting_started/routing_error_no_route_matches.pngbin5913 -> 0 bytes
-rw-r--r--guides/assets/images/habtm.pngbin47284 -> 0 bytes
-rw-r--r--guides/assets/images/has_many.pngbin24300 -> 0 bytes
-rw-r--r--guides/assets/images/has_many_through.pngbin78099 -> 0 bytes
-rw-r--r--guides/assets/images/has_one.pngbin27547 -> 0 bytes
-rw-r--r--guides/assets/images/has_one_through.pngbin70130 -> 0 bytes
-rw-r--r--guides/assets/images/header_backdrop.pngbin206 -> 0 bytes
-rw-r--r--guides/assets/images/icons/README5
-rw-r--r--guides/assets/images/icons/callouts/1.pngbin147 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/10.pngbin183 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/11.pngbin176 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/12.pngbin186 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/13.pngbin188 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/14.pngbin190 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/15.pngbin191 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/2.pngbin168 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/3.pngbin170 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/4.pngbin165 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/5.pngbin169 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/6.pngbin176 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/7.pngbin160 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/8.pngbin176 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/9.pngbin177 -> 0 bytes
-rw-r--r--guides/assets/images/icons/caution.pngbin2295 -> 0 bytes
-rw-r--r--guides/assets/images/icons/example.pngbin2052 -> 0 bytes
-rw-r--r--guides/assets/images/icons/home.pngbin1134 -> 0 bytes
-rw-r--r--guides/assets/images/icons/important.pngbin2426 -> 0 bytes
-rw-r--r--guides/assets/images/icons/next.pngbin1111 -> 0 bytes
-rw-r--r--guides/assets/images/icons/note.pngbin2096 -> 0 bytes
-rw-r--r--guides/assets/images/icons/prev.pngbin1093 -> 0 bytes
-rw-r--r--guides/assets/images/icons/tip.pngbin2170 -> 0 bytes
-rw-r--r--guides/assets/images/icons/up.pngbin1106 -> 0 bytes
-rw-r--r--guides/assets/images/icons/warning.pngbin2616 -> 0 bytes
-rw-r--r--guides/assets/images/oscardelben.jpgbin6299 -> 0 bytes
-rw-r--r--guides/assets/images/polymorphic.pngbin65417 -> 0 bytes
-rw-r--r--guides/assets/images/radar.pngbin17095 -> 0 bytes
-rw-r--r--guides/assets/images/rails_logo_remix.gifbin8533 -> 0 bytes
-rw-r--r--guides/assets/images/security/csrf.png (renamed from guides/assets/images/csrf.png)bin32179 -> 32179 bytes
-rw-r--r--guides/assets/images/security/session_fixation.png (renamed from guides/assets/images/session_fixation.png)bin38296 -> 38296 bytes
-rw-r--r--guides/assets/images/tab_yellow.pngbin1395 -> 0 bytes
-rw-r--r--guides/assets/images/vijaydev.jpgbin2897 -> 0 bytes
-rw-r--r--guides/assets/javascripts/guides.js91
-rw-r--r--guides/assets/javascripts/jquery.min.js4
-rw-r--r--guides/assets/javascripts/responsive-tables.js83
-rw-r--r--guides/assets/stylesheets/main.css16
-rw-r--r--guides/assets/stylesheets/print.css2
-rw-r--r--guides/assets/stylesheets/responsive-tables.css50
-rw-r--r--guides/bug_report_templates/action_controller_gem.rb6
-rw-r--r--guides/bug_report_templates/action_controller_master.rb5
-rw-r--r--guides/bug_report_templates/active_job_gem.rb5
-rw-r--r--guides/bug_report_templates/active_job_master.rb4
-rw-r--r--guides/bug_report_templates/active_record_gem.rb5
-rw-r--r--guides/bug_report_templates/active_record_master.rb4
-rw-r--r--guides/bug_report_templates/active_record_migrations_gem.rb7
-rw-r--r--guides/bug_report_templates/active_record_migrations_master.rb6
-rw-r--r--guides/bug_report_templates/benchmark.rb4
-rw-r--r--guides/bug_report_templates/generic_gem.rb6
-rw-r--r--guides/bug_report_templates/generic_master.rb4
-rw-r--r--guides/rails_guides/generator.rb52
-rw-r--r--guides/rails_guides/helpers.rb9
-rw-r--r--guides/rails_guides/kindle.rb6
-rw-r--r--guides/rails_guides/levenshtein.rb2
-rw-r--r--guides/rails_guides/markdown/renderer.rb2
-rw-r--r--guides/source/2_2_release_notes.md5
-rw-r--r--guides/source/2_3_release_notes.md14
-rw-r--r--guides/source/3_0_release_notes.md2
-rw-r--r--guides/source/3_2_release_notes.md6
-rw-r--r--guides/source/4_0_release_notes.md4
-rw-r--r--guides/source/4_1_release_notes.md2
-rw-r--r--guides/source/4_2_release_notes.md4
-rw-r--r--guides/source/5_0_release_notes.md2
-rw-r--r--guides/source/5_1_release_notes.md7
-rw-r--r--guides/source/5_2_release_notes.md861
-rw-r--r--guides/source/_welcome.html.erb8
-rw-r--r--guides/source/action_cable_overview.md29
-rw-r--r--guides/source/action_controller_overview.md54
-rw-r--r--guides/source/action_mailer_basics.md116
-rw-r--r--guides/source/action_view_overview.md32
-rw-r--r--guides/source/active_job_basics.md58
-rw-r--r--guides/source/active_model_basics.md2
-rw-r--r--guides/source/active_record_basics.md22
-rw-r--r--guides/source/active_record_callbacks.md4
-rw-r--r--guides/source/active_record_migrations.md85
-rw-r--r--guides/source/active_record_postgresql.md4
-rw-r--r--guides/source/active_record_querying.md24
-rw-r--r--guides/source/active_record_validations.md2
-rw-r--r--guides/source/active_storage_overview.md597
-rw-r--r--guides/source/active_support_core_extensions.md390
-rw-r--r--guides/source/active_support_instrumentation.md121
-rw-r--r--guides/source/api_app.md11
-rw-r--r--guides/source/api_documentation_guidelines.md6
-rw-r--r--guides/source/asset_pipeline.md45
-rw-r--r--guides/source/association_basics.md85
-rw-r--r--guides/source/autoloading_and_reloading_constants.md51
-rw-r--r--guides/source/caching_with_rails.md84
-rw-r--r--guides/source/command_line.md14
-rw-r--r--guides/source/configuring.md107
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md7
-rw-r--r--guides/source/credits.html.erb80
-rw-r--r--guides/source/documents.yaml21
-rw-r--r--guides/source/engines.md39
-rw-r--r--guides/source/form_helpers.md24
-rw-r--r--guides/source/getting_started.md87
-rw-r--r--guides/source/i18n.md47
-rw-r--r--guides/source/index.html.erb4
-rw-r--r--guides/source/initialization.md4
-rw-r--r--guides/source/kindle/rails_guides.opf.erb3
-rw-r--r--guides/source/kindle/toc.html.erb1
-rw-r--r--guides/source/kindle/toc.ncx.erb4
-rw-r--r--guides/source/layout.html.erb18
-rw-r--r--guides/source/layouts_and_rendering.md46
-rw-r--r--guides/source/maintenance_policy.md8
-rw-r--r--guides/source/plugins.md6
-rw-r--r--guides/source/rails_on_rack.md14
-rw-r--r--guides/source/routing.md69
-rw-r--r--guides/source/security.md271
-rw-r--r--guides/source/testing.md171
-rw-r--r--guides/source/threading_and_code_execution.md324
-rw-r--r--guides/source/upgrading_ruby_on_rails.md75
-rw-r--r--guides/source/working_with_javascript_in_rails.md68
-rw-r--r--rails.gemspec2
-rw-r--r--railties/.gitignore6
-rw-r--r--railties/CHANGELOG.md155
-rw-r--r--railties/MIT-LICENSE2
-rw-r--r--railties/RDOC_MAIN.rdoc53
-rw-r--r--railties/lib/minitest/rails_plugin.rb13
-rw-r--r--railties/lib/rails.rb8
-rw-r--r--railties/lib/rails/all.rb2
-rw-r--r--railties/lib/rails/api/task.rb2
-rw-r--r--railties/lib/rails/app_loader.rb4
-rw-r--r--railties/lib/rails/app_updater.rb11
-rw-r--r--railties/lib/rails/application.rb77
-rw-r--r--railties/lib/rails/application/bootstrap.rb2
-rw-r--r--railties/lib/rails/application/configuration.rb114
-rw-r--r--railties/lib/rails/application/default_middleware_stack.rb6
-rw-r--r--railties/lib/rails/application/finisher.rb2
-rw-r--r--railties/lib/rails/application_controller.rb11
-rw-r--r--railties/lib/rails/cli.rb6
-rw-r--r--railties/lib/rails/code_statistics.rb2
-rw-r--r--railties/lib/rails/command.rb2
-rw-r--r--railties/lib/rails/command/base.rb2
-rw-r--r--railties/lib/rails/command/behavior.rb40
-rw-r--r--railties/lib/rails/command/helpers/editor.rb35
-rw-r--r--railties/lib/rails/command/spellchecker.rb11
-rw-r--r--railties/lib/rails/commands.rb2
-rw-r--r--railties/lib/rails/commands/application/application_command.rb4
-rw-r--r--railties/lib/rails/commands/console/console_command.rb2
-rw-r--r--railties/lib/rails/commands/credentials/credentials_command.rb51
-rw-r--r--railties/lib/rails/commands/dbconsole/dbconsole_command.rb4
-rw-r--r--railties/lib/rails/commands/destroy/destroy_command.rb2
-rw-r--r--railties/lib/rails/commands/encrypted/encrypted_command.rb86
-rw-r--r--railties/lib/rails/commands/generate/generate_command.rb2
-rw-r--r--railties/lib/rails/commands/plugin/plugin_command.rb4
-rw-r--r--railties/lib/rails/commands/routes/routes_command.rb37
-rw-r--r--railties/lib/rails/commands/runner/runner_command.rb4
-rw-r--r--railties/lib/rails/commands/secrets/secrets_command.rb21
-rw-r--r--railties/lib/rails/commands/server/server_command.rb117
-rw-r--r--railties/lib/rails/commands/test/test_command.rb6
-rw-r--r--railties/lib/rails/configuration.rb4
-rw-r--r--railties/lib/rails/engine.rb10
-rw-r--r--railties/lib/rails/engine/commands.rb2
-rw-r--r--railties/lib/rails/engine/configuration.rb2
-rw-r--r--railties/lib/rails/engine/updater.rb4
-rw-r--r--railties/lib/rails/gem_version.rb4
-rw-r--r--railties/lib/rails/generators.rb16
-rw-r--r--railties/lib/rails/generators/actions.rb10
-rw-r--r--railties/lib/rails/generators/app_base.rb160
-rw-r--r--railties/lib/rails/generators/css/assets/assets_generator.rb2
-rw-r--r--railties/lib/rails/generators/css/scaffold/scaffold_generator.rb8
-rw-r--r--railties/lib/rails/generators/erb.rb2
-rw-r--r--railties/lib/rails/generators/erb/controller/controller_generator.rb2
-rw-r--r--railties/lib/rails/generators/erb/controller/templates/view.html.erb.tt (renamed from railties/lib/rails/generators/erb/controller/templates/view.html.erb)0
-rw-r--r--railties/lib/rails/generators/erb/mailer/mailer_generator.rb4
-rw-r--r--railties/lib/rails/generators/erb/mailer/templates/view.html.erb.tt (renamed from railties/lib/rails/generators/erb/mailer/templates/view.html.erb)0
-rw-r--r--railties/lib/rails/generators/erb/mailer/templates/view.text.erb.tt (renamed from railties/lib/rails/generators/erb/mailer/templates/view.text.erb)0
-rw-r--r--railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb4
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt (renamed from railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb)8
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb.tt (renamed from railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb)0
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt (renamed from railties/lib/rails/generators/erb/scaffold/templates/index.html.erb)8
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/new.html.erb.tt (renamed from railties/lib/rails/generators/erb/scaffold/templates/new.html.erb)0
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt (renamed from railties/lib/rails/generators/erb/scaffold/templates/show.html.erb)0
-rw-r--r--railties/lib/rails/generators/generated_attribute.rb4
-rw-r--r--railties/lib/rails/generators/js/assets/assets_generator.rb2
-rw-r--r--railties/lib/rails/generators/migration.rb9
-rw-r--r--railties/lib/rails/generators/model_helpers.rb2
-rw-r--r--railties/lib/rails/generators/named_base.rb52
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb74
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile.tt (renamed from railties/lib/rails/generators/rails/app/templates/Gemfile)21
-rw-r--r--railties/lib/rails/generators/rails/app/templates/README.md.tt (renamed from railties/lib/rails/generators/rails/app/templates/README.md)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Rakefile.tt (renamed from railties/lib/rails/generators/rails/app/templates/Rakefile)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js.tt (renamed from railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css.tt (renamed from railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt1
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/bundle.tt (renamed from railties/lib/rails/generators/rails/app/templates/bin/bundle)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/rails.tt (renamed from railties/lib/rails/generators/rails/app/templates/bin/rails)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/rake.tt (renamed from railties/lib/rails/generators/rails/app/templates/bin/rake)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/update.tt5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/yarn.tt (renamed from railties/lib/rails/generators/rails/app/templates/bin/yarn)6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config.ru.tt (renamed from railties/lib/rails/generators/rails/app/templates/config.ru)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/application.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/application.rb)9
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/boot.rb)2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/cable.yml)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environment.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/environment.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt11
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt10
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt12
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt25
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt27
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt10
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/puma.rb)24
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/routes.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/routes.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/secrets.yml32
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/spring.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/spring.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/storage.yml)13
-rw-r--r--railties/lib/rails/generators/rails/app/templates/gitignore.tt (renamed from railties/lib/rails/generators/rails/app/templates/gitignore)7
-rw-r--r--railties/lib/rails/generators/rails/app/templates/package.json.tt (renamed from railties/lib/rails/generators/rails/app/templates/package.json)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/ruby-version1
-rw-r--r--railties/lib/rails/generators/rails/app/templates/ruby-version.tt1
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/test/test_helper.rb)8
-rw-r--r--railties/lib/rails/generators/rails/assets/templates/stylesheet.css2
-rw-r--r--railties/lib/rails/generators/rails/controller/controller_generator.rb13
-rw-r--r--railties/lib/rails/generators/rails/controller/templates/controller.rb.tt (renamed from railties/lib/rails/generators/rails/controller/templates/controller.rb)0
-rw-r--r--railties/lib/rails/generators/rails/credentials/credentials_generator.rb38
-rw-r--r--railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb27
-rw-r--r--railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb72
-rw-r--r--railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb58
-rw-r--r--railties/lib/rails/generators/rails/helper/templates/helper.rb.tt (renamed from railties/lib/rails/generators/rails/helper/templates/helper.rb)0
-rw-r--r--railties/lib/rails/generators/rails/master_key/master_key_generator.rb50
-rw-r--r--railties/lib/rails/generators/rails/model/model_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/plugin/plugin_generator.rb14
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Gemfile.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/Gemfile)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/README.md.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/README.md)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/Rakefile)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt6
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/config/routes.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/config/routes.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/gitignore.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/gitignore)3
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb)2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb)2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/application.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/rails/application.rb)2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js)4
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb)3
-rw-r--r--railties/lib/rails/generators/rails/resource/USAGE2
-rw-r--r--railties/lib/rails/generators/rails/resource/resource_generator.rb4
-rw-r--r--railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt (renamed from railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb)0
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt (renamed from railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb)4
-rw-r--r--railties/lib/rails/generators/rails/task/templates/task.rb.tt (renamed from railties/lib/rails/generators/rails/task/templates/task.rb)0
-rw-r--r--railties/lib/rails/generators/resource_helpers.rb11
-rw-r--r--railties/lib/rails/generators/test_case.rb8
-rw-r--r--railties/lib/rails/generators/test_unit.rb2
-rw-r--r--railties/lib/rails/generators/test_unit/controller/controller_generator.rb2
-rw-r--r--railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/generator/generator_generator.rb2
-rw-r--r--railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/helper/helper_generator.rb2
-rw-r--r--railties/lib/rails/generators/test_unit/integration/integration_generator.rb2
-rw-r--r--railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/job/job_generator.rb9
-rw-r--r--railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb)0
-rw-r--r--railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb4
-rw-r--r--railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/mailer/templates/preview.rb.tt (renamed from railties/lib/rails/generators/test_unit/mailer/templates/preview.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/model/model_generator.rb2
-rw-r--r--railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt (renamed from railties/lib/rails/generators/test_unit/model/templates/fixtures.yml)0
-rw-r--r--railties/lib/rails/generators/test_unit/model/templates/unit_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/model/templates/unit_test.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb2
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb6
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/system/system_generator.rb2
-rw-r--r--railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb.tt (renamed from railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/system/templates/system_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/system/templates/system_test.rb)0
-rw-r--r--railties/lib/rails/generators/testing/behaviour.rb2
-rw-r--r--railties/lib/rails/info.rb2
-rw-r--r--railties/lib/rails/info_controller.rb2
-rw-r--r--railties/lib/rails/mailers_controller.rb14
-rw-r--r--railties/lib/rails/plugin/test.rb4
-rw-r--r--railties/lib/rails/rack/logger.rb6
-rw-r--r--railties/lib/rails/railtie.rb2
-rw-r--r--railties/lib/rails/railtie/configuration.rb4
-rw-r--r--railties/lib/rails/ruby_version_check.rb6
-rw-r--r--railties/lib/rails/secrets.rb18
-rw-r--r--railties/lib/rails/source_annotation_extractor.rb243
-rw-r--r--railties/lib/rails/tasks.rb1
-rw-r--r--railties/lib/rails/tasks/annotations.rake8
-rw-r--r--railties/lib/rails/tasks/dev.rake2
-rw-r--r--railties/lib/rails/tasks/engine.rake9
-rw-r--r--railties/lib/rails/tasks/framework.rake6
-rw-r--r--railties/lib/rails/tasks/routes.rake31
-rw-r--r--railties/lib/rails/tasks/statistics.rake2
-rw-r--r--railties/lib/rails/tasks/yarn.rake2
-rw-r--r--railties/lib/rails/templates/rails/mailers/email.html.erb46
-rw-r--r--railties/lib/rails/test_help.rb7
-rw-r--r--railties/lib/rails/test_unit/railtie.rb2
-rw-r--r--railties/lib/rails/test_unit/reporter.rb10
-rw-r--r--railties/lib/rails/test_unit/runner.rb2
-rw-r--r--railties/lib/rails/test_unit/testing.rake2
-rw-r--r--railties/lib/rails/welcome_controller.rb2
-rw-r--r--railties/railties.gemspec2
-rw-r--r--railties/test/app_loader_test.rb6
-rw-r--r--railties/test/application/asset_debugging_test.rb3
-rw-r--r--railties/test/application/assets_test.rb6
-rw-r--r--railties/test/application/configuration/custom_test.rb2
-rw-r--r--railties/test/application/configuration_test.rb229
-rw-r--r--railties/test/application/console_test.rb6
-rw-r--r--railties/test/application/content_security_policy_test.rb223
-rw-r--r--railties/test/application/dbconsole_test.rb52
-rw-r--r--railties/test/application/generators_test.rb3
-rw-r--r--railties/test/application/initializers/frameworks_test.rb4
-rw-r--r--railties/test/application/initializers/notifications_test.rb1
-rw-r--r--railties/test/application/integration_test_case_test.rb7
-rw-r--r--railties/test/application/loading_test.rb24
-rw-r--r--railties/test/application/mailer_previews_test.rb74
-rw-r--r--railties/test/application/middleware/cache_test.rb4
-rw-r--r--railties/test/application/middleware/cookies_test.rb145
-rw-r--r--railties/test/application/middleware/exceptions_test.rb2
-rw-r--r--railties/test/application/middleware/sendfile_test.rb2
-rw-r--r--railties/test/application/middleware/session_test.rb4
-rw-r--r--railties/test/application/middleware_test.rb6
-rw-r--r--railties/test/application/per_request_digest_cache_test.rb2
-rw-r--r--railties/test/application/rake/dbs_test.rb194
-rw-r--r--railties/test/application/rake/migrations_test.rb421
-rw-r--r--railties/test/application/rake/multi_dbs_test.rb164
-rw-r--r--railties/test/application/rake/notes_test.rb2
-rw-r--r--railties/test/application/rake_test.rb179
-rw-r--r--railties/test/application/rendering_test.rb2
-rw-r--r--railties/test/application/runner_test.rb16
-rw-r--r--railties/test/application/server_test.rb9
-rw-r--r--railties/test/application/test_runner_test.rb157
-rw-r--r--railties/test/application/test_test.rb5
-rw-r--r--railties/test/command/spellchecker_test.rb10
-rw-r--r--railties/test/commands/console_test.rb29
-rw-r--r--railties/test/commands/credentials_test.rb51
-rw-r--r--railties/test/commands/dbconsole_test.rb24
-rw-r--r--railties/test/commands/encrypted_test.rb106
-rw-r--r--railties/test/commands/routes_test.rb175
-rw-r--r--railties/test/commands/secrets_test.rb49
-rw-r--r--railties/test/commands/server_test.rb79
-rw-r--r--railties/test/engine/test_test.rb31
-rw-r--r--railties/test/engine_test.rb2
-rw-r--r--railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml1
-rw-r--r--railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb3
-rw-r--r--railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb3
-rw-r--r--railties/test/generators/actions_test.rb16
-rw-r--r--railties/test/generators/api_app_generator_test.rb21
-rw-r--r--railties/test/generators/app_generator_test.rb260
-rw-r--r--railties/test/generators/argv_scrubber_test.rb3
-rw-r--r--railties/test/generators/channel_generator_test.rb10
-rw-r--r--railties/test/generators/controller_generator_test.rb29
-rw-r--r--railties/test/generators/create_migration_test.rb2
-rw-r--r--railties/test/generators/encrypted_secrets_generator_test.rb44
-rw-r--r--railties/test/generators/generated_attribute_test.rb12
-rw-r--r--railties/test/generators/generators_test_helper.rb4
-rw-r--r--railties/test/generators/job_generator_test.rb10
-rw-r--r--railties/test/generators/model_generator_test.rb35
-rw-r--r--railties/test/generators/named_base_test.rb39
-rw-r--r--railties/test/generators/plugin_generator_test.rb58
-rw-r--r--railties/test/generators/scaffold_controller_generator_test.rb25
-rw-r--r--railties/test/generators/scaffold_generator_test.rb17
-rw-r--r--railties/test/generators/shared_generator_tests.rb130
-rw-r--r--railties/test/generators_test.rb19
-rw-r--r--railties/test/isolation/abstract_unit.rb118
-rw-r--r--railties/test/minitest/rails_plugin_test.rb42
-rw-r--r--railties/test/paths_test.rb28
-rw-r--r--railties/test/rack_logger_test.rb22
-rw-r--r--railties/test/rails_info_test.rb15
-rw-r--r--railties/test/railties/engine_test.rb82
-rw-r--r--railties/test/railties/railtie_test.rb14
-rw-r--r--railties/test/secrets_test.rb16
-rw-r--r--railties/test/test_unit/reporter_test.rb8
-rw-r--r--tasks/release.rb9
-rw-r--r--version.rb4
1804 files changed, 43889 insertions, 16611 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml
index 63d3562e9b..7d4ec1c54f 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -1,7 +1,29 @@
+checks:
+ argument-count:
+ enabled: false
+ complex-logic:
+ enabled: false
+ file-lines:
+ enabled: false
+ method-complexity:
+ enabled: false
+ method-count:
+ enabled: false
+ method-lines:
+ enabled: false
+ nested-control-flow:
+ enabled: false
+ return-statements:
+ enabled: false
+ similar-code:
+ enabled: false
+ identical-code:
+ enabled: false
+
engines:
rubocop:
enabled: true
- channel: rubocop-0-49
+ channel: rubocop-0-54
ratings:
paths:
diff --git a/.github/issue_template.md b/.github/issue_template.md
index 2d071d4a71..2ff6a271db 100644
--- a/.github/issue_template.md
+++ b/.github/issue_template.md
@@ -1,7 +1,7 @@
### 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))
+here](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#creating-a-bug-report))
### Expected behavior
Tell us what should happen
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 214d63740c..a36687ec99 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -16,6 +16,6 @@ CHANGELOG files by reviewers, please add the CHANGELOG entry at the top of the f
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)
+here](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)
Thanks for contributing to Rails!
diff --git a/.gitignore b/.gitignore
index da3b42cfbb..ab9ca8a6ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,25 +1,16 @@
-# Don't put *.swp, *.bak, etc here; those belong in a global ~/.gitignore.
+# Don't put *.swp, *.bak, etc here; those belong in a global .gitignore.
# Check out https://help.github.com/articles/ignoring-files for how to set that up.
.Gemfile
-.ruby-version
.byebug_history
-debug.log
-pkg
+.ruby-version
+/*/doc/
+/*/test/tmp/
/.bundle
-/dist
-/doc/rdoc
-/*/doc
-/*/test/tmp
-/activerecord/sqlnet.log
-/activemodel/test/fixtures/fixture_database.sqlite3
-/activesupport/test/fixtures/isolation_test
-/activestorage/test/service/configurations.yml
-/railties/test/500.html
-/railties/test/fixtures/tmp
-/railties/test/initializer/root/log
-/railties/doc
-/railties/tmp
-/guides/output
+/dist/
+/doc/
+/guides/output/
+debug.log
node_modules/
-/actionview/log
+package-lock.json
+pkg/
diff --git a/.rubocop.yml b/.rubocop.yml
index f3ed8bc841..954ab3b1cb 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,5 +1,7 @@
+require: './ci/custom_cops/lib/custom_cops'
+
AllCops:
- TargetRubyVersion: 2.2
+ TargetRubyVersion: 2.4
# RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
# to ignore them, so only the ones explicitly set in this file are enabled.
DisabledByDefault: true
@@ -7,6 +9,17 @@ AllCops:
- '**/templates/**/*'
- '**/vendor/**/*'
- 'actionpack/lib/action_dispatch/journey/parser.rb'
+ - 'railties/test/fixtures/tmp/**/*'
+
+# Prefer assert_not_x over refute_x
+CustomCops/RefuteNot:
+ Include:
+ - '**/test/**/*'
+
+# Prefer assert_not over assert !
+CustomCops/AssertNot:
+ Include:
+ - '**/test/**/*'
# Prefer &&/|| over and/or.
Style/AndOr:
@@ -26,6 +39,16 @@ Layout/CaseIndentation:
Layout/CommentIndentation:
Enabled: true
+Layout/ElseAlignment:
+ Enabled: true
+
+# Align `end` with the matching keyword or starting expression except for
+# assignments, where it should be aligned with the LHS.
+Layout/EndAlignment:
+ Enabled: true
+ EnforcedStyleAlignWith: variable
+ AutoCorrect: true
+
Layout/EmptyLineAfterMagicComment:
Enabled: true
@@ -58,6 +81,9 @@ Layout/IndentationConsistency:
Layout/IndentationWidth:
Enabled: true
+Layout/LeadingCommentSpace:
+ Enabled: true
+
Layout/SpaceAfterColon:
Enabled: true
@@ -73,9 +99,15 @@ Layout/SpaceAroundKeyword:
Layout/SpaceAroundOperators:
Enabled: true
+Layout/SpaceBeforeComma:
+ Enabled: true
+
Layout/SpaceBeforeFirstArg:
Enabled: true
+Style/DefWithParentheses:
+ Enabled: true
+
# Defining a method with parameters needs parentheses.
Style/MethodDefParentheses:
Enabled: true
@@ -126,12 +158,18 @@ Layout/TrailingWhitespace:
Style/UnneededPercentQ:
Enabled: true
-# Align `end` with the matching keyword or starting expression except for
-# assignments, where it should be aligned with the LHS.
-Lint/EndAlignment:
- Enabled: true
- EnforcedStyleAlignWith: variable
-
# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
Lint/RequireParentheses:
Enabled: true
+
+Style/RedundantReturn:
+ Enabled: true
+ AllowMultipleReturnValues: true
+
+Style/Semicolon:
+ Enabled: true
+ AllowAsExpressionSeparator: true
+
+# Prefer Foo.method over Foo::method
+Style/ColonMethodCall:
+ Enabled: true
diff --git a/.travis.yml b/.travis.yml
index 27400b98c7..0fdea1367c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,8 +2,8 @@ language: ruby
sudo: false
cache:
- bundler: true
directories:
+ - vendor/bundle
- /tmp/cache/unicode_conformance
- /tmp/beanstalkd-1.10
- node_modules
@@ -11,29 +11,38 @@ cache:
services:
- memcached
+ - redis-server
addons:
postgresql: "9.6"
+ chrome: stable
+ apt:
+ sources:
+ - sourceline: "ppa:mc3man/trusty-media"
+ - sourceline: "ppa:ubuntuhandbook1/apps"
+ packages:
+ - ffmpeg
+ - mupdf
+ - mupdf-tools
+ - poppler-utils
bundler_args: --without test --jobs 3 --retry 3
before_install:
- "rm ${BUNDLE_GEMFILE}.lock"
- "travis_retry gem update --system"
- - "travis_retry gem update bundler"
+ - "travis_retry gem install bundler"
- "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)"
- "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd"
- - "[[ -z $encrypted_8a915ebdd931_key && -z $encrypted_8a915ebdd931_iv ]] || openssl aes-256-cbc -K $encrypted_8a915ebdd931_key -iv $encrypted_8a915ebdd931_iv -in activestorage/test/service/configurations.yml.enc -out activestorage/test/service/configurations.yml -d"
+ - "[[ -z $encrypted_0fb9444d0374_key && -z $encrypted_0fb9444d0374_iv ]] || openssl aes-256-cbc -K $encrypted_0fb9444d0374_key -iv $encrypted_0fb9444d0374_iv -in activestorage/test/service/configurations.yml.enc -out activestorage/test/service/configurations.yml -d"
- "[[ $GEM != 'av:ujs' ]] || nvm install node"
- "[[ $GEM != 'av:ujs' ]] || node --version"
- "[[ $GEM != 'av:ujs' ]] || (cd actionview && npm install)"
- - "[[ $GEM != 'av:ujs' ]] || [[ $(phantomjs --version) > '2' ]] || npm install -g phantomjs-prebuilt"
before_script:
# Set Sauce Labs username and access key. Obfuscated, purposefully not encrypted.
# Decodes to e.g. `export VARIABLE=VALUE`
- $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX0FDQ0VTU19LRVk9YTAzNTM0M2YtZTkyMi00MGIzLWFhM2MtMDZiM2VhNjM1YzQ4")
- $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX1VTRVJOQU1FPXJ1YnlvbnJhaWxz")
- - redis-server --bind 127.0.0.1 --port 6379 --requirepass 'password' --daemonize yes
script: 'ci/travis.rb'
@@ -52,59 +61,56 @@ env:
- "GEM=ac:integration"
rvm:
- - 2.2.8
- - 2.3.5
- - 2.4.2
+ - 2.4.4
+ - 2.5.1
- ruby-head
matrix:
include:
- - rvm: 2.4.2
+ - rvm: 2.5.1
env: "GEM=av:ujs"
- - rvm: 2.2.8
+ - rvm: 2.4.4
env: "GEM=aj:integration"
services:
- memcached
+ - redis-server
- rabbitmq
- - rvm: 2.3.5
- env: "GEM=aj:integration"
- services:
- - memcached
- - rabbitmq
- - rvm: 2.4.2
+ - rvm: 2.5.1
env: "GEM=aj:integration"
services:
- memcached
+ - redis-server
- rabbitmq
- rvm: ruby-head
env: "GEM=aj:integration"
services:
- memcached
+ - redis-server
- rabbitmq
- - rvm: 2.3.5
+ - rvm: 2.5.1
env:
- "GEM=ar:mysql2 MYSQL=mariadb"
addons:
mariadb: 10.2
- - rvm: 2.3.5
+ - rvm: 2.5.1
env:
- "GEM=ar:sqlite3_mem"
- - rvm: 2.3.5
+ - rvm: 2.5.1
env:
- "GEM=ar:postgresql POSTGRES=9.2"
addons:
postgresql: "9.2"
- - rvm: jruby-9.1.13.0
+ - rvm: jruby-head
jdk: oraclejdk8
env:
- "GEM=ap"
- - rvm: jruby-9.1.13.0
+ - rvm: jruby-head
jdk: oraclejdk8
env:
- "GEM=am,amo,aj"
allow_failures:
- rvm: ruby-head
- - rvm: jruby-9.1.13.0
+ - rvm: jruby-head
- env: "GEM=ac:integration"
fast_finish: true
diff --git a/Brewfile b/Brewfile
new file mode 100644
index 0000000000..4ac325e80a
--- /dev/null
+++ b/Brewfile
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+tap "homebrew/core"
+tap "homebrew/bundle"
+tap "homebrew/services"
+tap "caskroom/cask"
+brew "ffmpeg"
+brew "memcached"
+brew "mysql"
+brew "postgresql"
+brew "redis"
+brew "yarn"
+cask "xquartz"
+brew "mupdf"
+brew "poppler"
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 078d5f1219..ecd56b87d6 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -8,5 +8,5 @@ http://rubyonrails.org/conduct/
For a history of updates, see the page history here:
-https://github.com/rails/rails.github.com/commits/master/conduct/index.html
+https://github.com/rails/homepage/commits/master/conduct.html
diff --git a/Gemfile b/Gemfile
index fda547fab2..1b7ea9b49f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,21 +6,16 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gemspec
-gem "arel", github: "rails/arel"
-
# We need a newish Rake since Active Job sets its test tasks' descriptions.
gem "rake", ">= 11.1"
-# 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
+gem "mocha"
-gem "capybara", "~> 2.13"
+gem "capybara", ">= 2.15"
gem "rack-cache", "~> 1.2"
-gem "jquery-rails"
gem "coffee-rails"
-gem "sass-rails", github: "rails/sass-rails", branch: "5-0-stable"
+gem "sass-rails"
gem "turbolinks", "~> 5"
# require: false so bcrypt is loaded only when has_secure_password is used.
@@ -41,19 +36,17 @@ gem "rubocop", ">= 0.47", require: false
gem "rb-inotify", github: "matthewd/rb-inotify", branch: "close-handling", require: false
group :doc do
- gem "sdoc", github: "robin850/sdoc", branch: "upgrade"
+ gem "sdoc", "~> 1.0"
gem "redcarpet", "~> 3.2.3", platforms: :ruby
gem "w3c_validators"
gem "kindlerb", "~> 1.2.0"
end
# Active Support.
-gem "dalli", ">= 2.2.1"
+gem "dalli"
gem "listen", ">= 3.0.5", "< 3.2", require: false
gem "libxml-ruby", platforms: :ruby
-
-# Action View. For testing Erubis handler deprecation.
-gem "erubis", "~> 2.7.0", require: false
+gem "connection_pool", require: false
# for railties app_generator_test
gem "bootsnap", ">= 1.1.0", require: false
@@ -65,13 +58,10 @@ group :job do
gem "sidekiq", require: false
gem "sucker_punch", require: false
gem "delayed_job", require: false
- gem "queue_classic", github: "QueueClassic/queue_classic", branch: "master", require: false, platforms: :ruby
+ gem "queue_classic", github: "rafaelfranca/queue_classic", branch: "update-pg", require: false, platforms: :ruby
gem "sneakers", require: false
gem "que", require: false
gem "backburner", require: false
- #TODO: add qu after it support Rails 5.1
- # gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false
- # gem "qu-redis", require: false
gem "delayed_job_active_record", require: false
gem "sequel", require: false
end
@@ -80,9 +70,10 @@ end
group :cable do
gem "puma", require: false
- gem "em-hiredis", require: false
gem "hiredis", require: false
- gem "redis", require: false
+ gem "redis", "~> 4.0", require: false
+
+ gem "redis-namespace"
gem "websocket-client-simple", github: "matthewd/websocket-client-simple", branch: "close-race", require: false
@@ -91,12 +82,19 @@ group :cable do
gem "sprockets-export", require: false
end
+# Active Storage
group :storage do
gem "aws-sdk-s3", require: false
- gem "google-cloud-storage", "~> 1.3", require: false
+ gem "google-cloud-storage", "~> 1.8", require: false
gem "azure-storage", require: false
- gem "mini_magick"
+ gem "image_processing", "~> 1.2"
+ gem "ffi", "<= 1.9.21"
+end
+
+group :ujs do
+ gem "qunit-selenium"
+ gem "chromedriver-helper"
end
# Add your own local bundler stuff.
@@ -115,7 +113,7 @@ group :test do
end
platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do
- gem "nokogiri", ">= 1.6.8"
+ gem "nokogiri", ">= 1.8.1"
# Needed for compiling the ActionDispatch::Journey parser.
gem "racc", ">=1.4.6", require: false
@@ -125,7 +123,7 @@ platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do
group :db do
gem "pg", ">= 0.18.0"
- gem "mysql2", ">= 0.4.4"
+ gem "mysql2", ">= 0.4.10"
end
end
@@ -148,7 +146,7 @@ end
platforms :rbx do
# The rubysl-yaml gem doesn't ship with Psych by default as it needs
# libyaml that isn't always available.
- gem "psych", "~> 2.0"
+ gem "psych", "~> 3.0"
end
# Gems that are necessary for Active Record tests with Oracle.
diff --git a/Gemfile.lock b/Gemfile.lock
index 787631ada5..3c73f45626 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,12 +1,4 @@
GIT
- remote: https://github.com/QueueClassic/queue_classic.git
- revision: cde82d17ded2799ed726dd7b0df6ce1fd4c1b7da
- branch: master
- specs:
- queue_classic (3.2.0.RC1)
- pg (>= 0.17, < 0.20)
-
-GIT
remote: https://github.com/matthewd/rb-inotify.git
revision: 856730aad4b285969e8dd621e44808a7c5af4242
branch: close-handling
@@ -24,90 +16,72 @@ GIT
websocket
GIT
- remote: https://github.com/rails/arel.git
- revision: 42510bf71472e2e35d9becb546edd05562672344
- specs:
- arel (9.0.0.alpha)
-
-GIT
- remote: https://github.com/rails/sass-rails.git
- revision: bb5c1d34e8acad2e2960cc785184ffe17d7b3bca
- branch: 5-0-stable
- specs:
- sass-rails (5.0.6)
- railties (>= 4.0.0, < 6)
- sass (~> 3.1)
- sprockets (>= 2.8, < 4.0)
- sprockets-rails (>= 2.0, < 4.0)
- tilt (>= 1.1, < 3)
-
-GIT
- remote: https://github.com/robin850/sdoc.git
- revision: 0e340352f3ab2f196c8a8743f83c2ee286e4f71c
- branch: upgrade
+ remote: https://github.com/rafaelfranca/queue_classic.git
+ revision: dee64b361355d56700ad7aa3b151bf653a617526
+ branch: update-pg
specs:
- sdoc (1.0.0.rc2)
- rdoc (~> 5.0)
+ queue_classic (3.2.0.RC1)
+ pg (>= 0.17, < 2.0)
PATH
remote: .
specs:
- actioncable (5.2.0.alpha)
- actionpack (= 5.2.0.alpha)
+ actioncable (6.0.0.alpha)
+ actionpack (= 6.0.0.alpha)
nio4r (~> 2.0)
- websocket-driver (~> 0.6.1)
- actionmailer (5.2.0.alpha)
- actionpack (= 5.2.0.alpha)
- actionview (= 5.2.0.alpha)
- activejob (= 5.2.0.alpha)
+ websocket-driver (>= 0.6.1)
+ actionmailer (6.0.0.alpha)
+ actionpack (= 6.0.0.alpha)
+ actionview (= 6.0.0.alpha)
+ activejob (= 6.0.0.alpha)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (5.2.0.alpha)
- actionview (= 5.2.0.alpha)
- activesupport (= 5.2.0.alpha)
+ actionpack (6.0.0.alpha)
+ actionview (= 6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (5.2.0.alpha)
- activesupport (= 5.2.0.alpha)
+ actionview (6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
- activejob (5.2.0.alpha)
- activesupport (= 5.2.0.alpha)
+ activejob (6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
globalid (>= 0.3.6)
- activemodel (5.2.0.alpha)
- activesupport (= 5.2.0.alpha)
- activerecord (5.2.0.alpha)
- activemodel (= 5.2.0.alpha)
- activesupport (= 5.2.0.alpha)
- arel (= 9.0.0.alpha)
- activestorage (5.2.0.alpha)
- actionpack (= 5.2.0.alpha)
- activerecord (= 5.2.0.alpha)
- activesupport (5.2.0.alpha)
+ activemodel (6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
+ activerecord (6.0.0.alpha)
+ activemodel (= 6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
+ activestorage (6.0.0.alpha)
+ actionpack (= 6.0.0.alpha)
+ activerecord (= 6.0.0.alpha)
+ marcel (~> 0.3.1)
+ activesupport (6.0.0.alpha)
concurrent-ruby (~> 1.0, >= 1.0.2)
- i18n (~> 0.7)
+ i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
- rails (5.2.0.alpha)
- actioncable (= 5.2.0.alpha)
- actionmailer (= 5.2.0.alpha)
- actionpack (= 5.2.0.alpha)
- actionview (= 5.2.0.alpha)
- activejob (= 5.2.0.alpha)
- activemodel (= 5.2.0.alpha)
- activerecord (= 5.2.0.alpha)
- activestorage (= 5.2.0.alpha)
- activesupport (= 5.2.0.alpha)
+ rails (6.0.0.alpha)
+ actioncable (= 6.0.0.alpha)
+ actionmailer (= 6.0.0.alpha)
+ actionpack (= 6.0.0.alpha)
+ actionview (= 6.0.0.alpha)
+ activejob (= 6.0.0.alpha)
+ activemodel (= 6.0.0.alpha)
+ activerecord (= 6.0.0.alpha)
+ activestorage (= 6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
bundler (>= 1.3.0)
- railties (= 5.2.0.alpha)
+ railties (= 6.0.0.alpha)
sprockets-rails (>= 2.0.0)
- railties (5.2.0.alpha)
- actionpack (= 5.2.0.alpha)
- activesupport (= 5.2.0.alpha)
+ railties (6.0.0.alpha)
+ actionpack (= 6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
@@ -115,10 +89,23 @@ PATH
GEM
remote: https://rubygems.org/
specs:
- addressable (2.5.1)
- public_suffix (~> 2.0, >= 2.0.2)
+ activerecord-jdbc-adapter (1.3.24)
+ activerecord (>= 2.2, < 5.0)
+ activerecord-jdbcmysql-adapter (1.3.24)
+ activerecord-jdbc-adapter (~> 1.3.24)
+ jdbc-mysql (>= 5.1.22)
+ activerecord-jdbcpostgresql-adapter (1.3.24)
+ activerecord-jdbc-adapter (~> 1.3.24)
+ jdbc-postgres (~> 9.1, <= 9.4.1206)
+ activerecord-jdbcsqlite3-adapter (1.3.24)
+ activerecord-jdbc-adapter (~> 1.3.24)
+ jdbc-sqlite3 (>= 3.7.2, < 3.9)
+ addressable (2.5.2)
+ public_suffix (>= 2.0.2, < 4.0)
amq-protocol (2.2.0)
- ast (2.3.0)
+ archive-zip (0.7.0)
+ io-like (~> 0.3.0)
+ ast (2.4.0)
aws-partitions (1.20.0)
aws-sdk-core (3.3.0)
aws-partitions (~> 1.0)
@@ -145,6 +132,7 @@ GEM
concurrent-ruby (~> 1.0.1)
dante (> 0.1.5)
bcrypt (3.1.11)
+ bcrypt (3.1.11-java)
bcrypt (3.1.11-x64-mingw32)
bcrypt (3.1.11-x86-mingw32)
beaneater (1.0.0)
@@ -166,21 +154,26 @@ GEM
childprocess
faraday
selenium-webdriver
- bootsnap (1.1.2)
+ bootsnap (1.2.1)
+ msgpack (~> 1.0)
+ bootsnap (1.2.1-java)
msgpack (~> 1.0)
builder (3.2.3)
bunny (2.6.6)
amq-protocol (>= 2.1.0)
byebug (9.0.6)
- capybara (2.15.1)
+ capybara (3.0.1)
addressable
mini_mime (>= 0.1.3)
- nokogiri (>= 1.3.3)
- rack (>= 1.0.0)
- rack-test (>= 0.5.4)
- xpath (~> 2.0)
+ nokogiri (~> 1.8)
+ rack (>= 1.6.0)
+ rack-test (>= 0.6.3)
+ xpath (~> 3.0)
childprocess (0.7.1)
ffi (~> 1.0, >= 1.0.11)
+ chromedriver-helper (1.1.0)
+ archive-zip (~> 0.7.0)
+ nokogiri (~> 1.6)
coffee-rails (4.2.2)
coffee-script (>= 2.2.0)
railties (>= 4.0.0)
@@ -189,39 +182,35 @@ GEM
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.0.5)
+ concurrent-ruby (1.0.5-java)
connection_pool (2.2.1)
cookiejar (0.3.3)
+ crass (1.0.3)
curses (1.0.2)
daemons (1.2.4)
- dalli (2.7.6)
+ dalli (2.7.8)
dante (0.2.0)
- declarative (0.0.9)
+ declarative (0.0.10)
declarative-option (0.1.0)
- delayed_job (4.1.3)
+ delayed_job (4.1.4)
activesupport (>= 3.0, < 5.2)
delayed_job_active_record (4.1.2)
activerecord (>= 3.0, < 5.2)
delayed_job (>= 3.0, < 5)
digest-crc (0.4.1)
- em-hiredis (0.3.1)
- eventmachine (~> 1.0)
- hiredis (~> 0.6.0)
em-http-request (1.1.5)
addressable (>= 2.3.4)
cookiejar (!= 0.3.1)
em-socksify (>= 0.3)
eventmachine (>= 1.0.3)
http_parser.rb (>= 0.6.0)
- em-socksify (0.3.1)
+ em-socksify (0.3.2)
eventmachine (>= 1.0.0.beta.4)
- erubi (1.6.1)
- erubis (2.7.0)
- et-orbi (1.0.5)
+ erubi (1.7.0)
+ et-orbi (1.0.8)
tzinfo
event_emitter (0.2.6)
eventmachine (1.2.5)
- eventmachine (1.2.5-x64-mingw32)
- eventmachine (1.2.5-x86-mingw32)
execjs (2.7.0)
faraday (0.13.1)
multipart-post (>= 1.2, < 3)
@@ -238,46 +227,53 @@ GEM
faye-websocket (0.10.7)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
- ffi (1.9.18)
- ffi (1.9.18-x64-mingw32)
- ffi (1.9.18-x86-mingw32)
- globalid (0.4.0)
+ ffi (1.9.21)
+ ffi (1.9.21-java)
+ ffi (1.9.21-x64-mingw32)
+ ffi (1.9.21-x86-mingw32)
+ globalid (0.4.1)
activesupport (>= 4.2.0)
- google-api-client (0.13.1)
+ google-api-client (0.17.3)
addressable (~> 2.5, >= 2.5.1)
- googleauth (~> 0.5)
+ googleauth (>= 0.5, < 0.7.0)
httpclient (>= 2.8.1, < 3.0)
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
- google-cloud-core (1.0.0)
+ google-cloud-core (1.1.0)
google-cloud-env (~> 1.0)
- googleauth (~> 0.5.1)
google-cloud-env (1.0.1)
faraday (~> 0.11)
- google-cloud-storage (1.4.0)
+ google-cloud-storage (1.9.0)
digest-crc (~> 0.4)
- google-api-client (~> 0.13.0)
- google-cloud-core (~> 1.0)
- googleauth (0.5.3)
+ google-api-client (~> 0.17.0)
+ google-cloud-core (~> 1.1)
+ googleauth (~> 0.6.2)
+ googleauth (0.6.2)
faraday (~> 0.12)
- jwt (~> 1.4)
+ jwt (>= 1.4, < 3.0)
logging (~> 2.0)
memoist (~> 0.12)
multi_json (~> 1.11)
os (~> 0.9)
signet (~> 0.7)
hiredis (0.6.1)
+ hiredis (0.6.1-java)
http_parser.rb (0.6.0)
httpclient (2.8.3)
- i18n (0.8.6)
+ i18n (1.0.0)
+ concurrent-ruby (~> 1.0)
+ image_processing (1.2.0)
+ mini_magick (~> 4.0)
+ ruby-vips (>= 2.0.10, < 3)
+ io-like (0.3.0)
+ jdbc-mysql (5.1.44)
+ jdbc-postgres (9.4.1206)
+ jdbc-sqlite3 (3.8.11.2)
jmespath (1.3.1)
- jquery-rails (4.3.1)
- rails-dom-testing (>= 1, < 3)
- railties (>= 4.2.0)
- thor (>= 0.14, < 2.0)
json (2.1.0)
- jwt (1.5.6)
+ json (2.1.0-java)
+ jwt (2.1.0)
kindlerb (1.2.0)
mustache
nokogiri
@@ -290,80 +286,90 @@ GEM
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
- loofah (2.0.3)
+ loofah (2.2.1)
+ crass (~> 1.0.2)
nokogiri (>= 1.5.9)
- mail (2.6.6)
- mime-types (>= 1.16, < 4)
+ mail (2.7.0)
+ mini_mime (>= 0.1.1)
+ marcel (0.3.1)
+ mimemagic (~> 0.3.2)
memoist (0.16.0)
metaclass (0.0.4)
- method_source (0.8.2)
+ method_source (0.9.0)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
+ mimemagic (0.3.2)
mini_magick (4.8.0)
- mini_mime (0.1.4)
- mini_portile2 (2.2.0)
- minitest (5.10.3)
+ mini_mime (1.0.0)
+ mini_portile2 (2.3.0)
+ minitest (5.11.3)
minitest-bisect (1.4.0)
minitest-server (~> 1.0)
path_expander (~> 1.0)
- minitest-server (1.0.4)
+ minitest-server (1.0.5)
minitest (~> 5.0)
- mocha (0.14.0)
+ mocha (1.5.0)
metaclass (~> 0.0.1)
mono_logger (1.1.0)
- msgpack (1.1.0)
- msgpack (1.1.0-x64-mingw32)
- msgpack (1.1.0-x86-mingw32)
- multi_json (1.12.1)
+ msgpack (1.2.4)
+ msgpack (1.2.4-java)
+ msgpack (1.2.4-x64-mingw32)
+ msgpack (1.2.4-x86-mingw32)
+ multi_json (1.12.2)
multipart-post (2.0.0)
mustache (1.0.5)
- mustermann (1.0.0)
- mysql2 (0.4.9)
- mysql2 (0.4.9-x64-mingw32)
- mysql2 (0.4.9-x86-mingw32)
- nio4r (2.1.0)
- nokogiri (1.8.0)
- mini_portile2 (~> 2.2.0)
- nokogiri (1.8.0-x64-mingw32)
- mini_portile2 (~> 2.2.0)
- nokogiri (1.8.0-x86-mingw32)
- mini_portile2 (~> 2.2.0)
+ mustermann (1.0.2)
+ mysql2 (0.5.0)
+ mysql2 (0.5.0-x64-mingw32)
+ mysql2 (0.5.0-x86-mingw32)
+ nio4r (2.2.0)
+ nio4r (2.2.0-java)
+ nokogiri (1.8.2)
+ mini_portile2 (~> 2.3.0)
+ nokogiri (1.8.2-java)
+ nokogiri (1.8.2-x64-mingw32)
+ mini_portile2 (~> 2.3.0)
+ nokogiri (1.8.2-x86-mingw32)
+ mini_portile2 (~> 2.3.0)
os (0.9.6)
- parallel (1.12.0)
- parser (2.4.0.0)
- ast (~> 2.2)
+ parallel (1.12.1)
+ parser (2.5.1.0)
+ ast (~> 2.4.0)
path_expander (1.0.2)
- pg (0.19.0)
- pg (0.19.0-x64-mingw32)
- pg (0.19.0-x86-mingw32)
+ pg (1.0.0)
+ pg (1.0.0-x64-mingw32)
+ pg (1.0.0-x86-mingw32)
powerpack (0.1.1)
- psych (2.2.4)
- public_suffix (2.0.5)
+ psych (3.0.2)
+ public_suffix (3.0.2)
puma (3.9.1)
+ puma (3.9.1-java)
que (0.14.0)
+ qunit-selenium (0.0.4)
+ selenium-webdriver
+ thor
racc (1.4.14)
- rack (2.0.3)
+ rack (2.0.4)
rack-cache (1.7.0)
rack (>= 0.4)
- rack-protection (2.0.0)
+ rack-protection (2.0.1)
rack
- rack-test (0.7.0)
+ rack-test (1.0.0)
rack (>= 1.0, < 3)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
- rainbow (2.2.2)
- rake
- rake (12.0.0)
+ rainbow (3.0.0)
+ rake (12.3.0)
rb-fsevent (0.10.2)
- rdoc (5.1.0)
+ rdoc (6.0.1)
redcarpet (3.2.3)
- redis (3.3.3)
- redis-namespace (1.5.3)
- redis (~> 3.0, >= 3.0.4)
+ redis (4.0.1)
+ redis-namespace (1.6.0)
+ redis (>= 3.0.4)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
@@ -374,50 +380,60 @@ GEM
redis-namespace (~> 1.3)
sinatra (>= 0.9.2)
vegas (~> 0.1.2)
- resque-scheduler (4.3.0)
+ resque-scheduler (4.3.1)
mono_logger (~> 1.0)
- redis (~> 3.3)
+ redis (>= 3.3, < 5)
resque (~> 1.26)
rufus-scheduler (~> 3.2)
retriable (3.1.1)
- rubocop (0.49.1)
+ rubocop (0.54.0)
parallel (~> 1.10)
- parser (>= 2.3.3.1, < 3.0)
+ parser (>= 2.5)
powerpack (~> 0.1)
- rainbow (>= 1.99.1, < 3.0)
+ rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
- ruby-progressbar (1.8.1)
+ ruby-progressbar (1.9.0)
+ ruby-vips (2.0.10)
+ ffi (~> 1.9)
ruby_dep (1.5.0)
rubyzip (1.2.1)
rufus-scheduler (3.4.2)
et-orbi (~> 1.0)
- sass (3.5.1)
+ sass (3.5.3)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
+ sass-rails (5.0.7)
+ railties (>= 4.0.0, < 6)
+ sass (~> 3.1)
+ sprockets (>= 2.8, < 4.0)
+ sprockets-rails (>= 2.0, < 4.0)
+ tilt (>= 1.1, < 3)
+ sdoc (1.0.0)
+ rdoc (>= 5.0)
selenium-webdriver (3.5.1)
childprocess (~> 0.5)
rubyzip (~> 1.0)
sequel (4.49.0)
serverengine (1.5.11)
sigdump (~> 0.2.2)
- sidekiq (5.0.4)
+ sidekiq (5.0.5)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
- redis (~> 3.3, >= 3.3.3)
+ redis (>= 3.3.4, < 5)
sigdump (0.2.4)
- signet (0.7.3)
+ signet (0.8.1)
addressable (~> 2.3)
faraday (~> 0.9)
- jwt (~> 1.5)
+ jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
- sinatra (2.0.0)
+ sinatra (2.0.1)
mustermann (~> 1.0)
rack (~> 2.0)
- rack-protection (= 2.0.0)
+ rack-protection (= 2.0.1)
tilt (~> 2.0)
sneakers (2.5.0)
bunny (~> 2.6.4)
@@ -445,10 +461,11 @@ GEM
thor (0.20.0)
thread (0.1.7)
thread_safe (0.3.6)
+ thread_safe (0.3.6-java)
tilt (2.0.8)
- turbolinks (5.0.1)
- turbolinks-source (~> 5)
- turbolinks-source (5.0.3)
+ turbolinks (5.1.1)
+ turbolinks-source (~> 5.1)
+ turbolinks-source (5.1.0)
tzinfo (1.2.3)
thread_safe (~> 0.1)
tzinfo-data (1.2017.2)
@@ -456,7 +473,7 @@ GEM
uber (0.1.0)
uglifier (3.2.0)
execjs (>= 0.3.0, < 3)
- unicode-display_width (1.3.0)
+ unicode-display_width (1.3.2)
useragent (0.16.8)
vegas (0.1.11)
rack (>= 1.0.0)
@@ -467,11 +484,14 @@ GEM
websocket (1.2.4)
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
+ websocket-driver (0.6.5-java)
+ websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
- xpath (2.1.0)
- nokogiri (~> 1.3)
+ xpath (3.0.0)
+ nokogiri (~> 1.8)
PLATFORMS
+ java
ruby
x64-mingw32
x86-mingw32
@@ -480,7 +500,6 @@ DEPENDENCIES
activerecord-jdbcmysql-adapter (>= 1.3.0)
activerecord-jdbcpostgresql-adapter (>= 1.3.0)
activerecord-jdbcsqlite3-adapter (>= 1.3.0)
- arel!
aws-sdk-s3
azure-storage
backburner
@@ -490,42 +509,44 @@ DEPENDENCIES
blade-sauce_labs_plugin
bootsnap (>= 1.1.0)
byebug
- capybara (~> 2.13)
+ capybara (>= 2.15)
+ chromedriver-helper
coffee-rails
- dalli (>= 2.2.1)
+ connection_pool
+ dalli
delayed_job
delayed_job_active_record
- em-hiredis
- erubis (~> 2.7.0)
- google-cloud-storage (~> 1.3)
+ ffi (<= 1.9.21)
+ google-cloud-storage (~> 1.8)
hiredis
- jquery-rails
+ image_processing (~> 1.2)
json (>= 2.0.0)
kindlerb (~> 1.2.0)
libxml-ruby
listen (>= 3.0.5, < 3.2)
- mini_magick
minitest-bisect
- mocha (~> 0.14)
- mysql2 (>= 0.4.4)
- nokogiri (>= 1.6.8)
+ mocha
+ mysql2 (>= 0.4.10)
+ nokogiri (>= 1.8.1)
pg (>= 0.18.0)
- psych (~> 2.0)
+ psych (~> 3.0)
puma
que
queue_classic!
+ qunit-selenium
racc (>= 1.4.6)
rack-cache (~> 1.2)
rails!
rake (>= 11.1)
rb-inotify!
redcarpet (~> 3.2.3)
- redis
+ redis (~> 4.0)
+ redis-namespace
resque
resque-scheduler
rubocop (>= 0.47)
- sass-rails!
- sdoc!
+ sass-rails
+ sdoc (~> 1.0)
sequel
sidekiq
sneakers
@@ -541,4 +562,4 @@ DEPENDENCIES
websocket-client-simple!
BUNDLED WITH
- 1.15.4
+ 1.16.1
diff --git a/MIT-LICENSE b/MIT-LICENSE
index 6b3cead1a7..8f769c0767 100644
--- a/MIT-LICENSE
+++ b/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2005-2017 David Heinemeier Hansson
+Copyright (c) 2005-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/RAILS_VERSION b/RAILS_VERSION
index d5ebf861d3..747766c587 100644
--- a/RAILS_VERSION
+++ b/RAILS_VERSION
@@ -1 +1 @@
-5.2.0.alpha
+6.0.0.alpha
diff --git a/README.md b/README.md
index 030dd405cb..c7ecaf15cf 100644
--- a/README.md
+++ b/README.md
@@ -1,48 +1,55 @@
# Welcome to Rails
+## What's Rails
+
Rails is a web-application framework that includes everything needed to
create database-backed web applications according to the
[Model-View-Controller (MVC)](http://en.wikipedia.org/wiki/Model-view-controller)
pattern.
Understanding the MVC pattern is key to understanding Rails. MVC divides your
-application into three layers, each with a specific responsibility.
+application into three layers: Model, View, and Controller, each with a specific responsibility.
+
+## Model layer
-The _Model layer_ represents your domain model (such as Account, Product,
-Person, Post, etc.) and encapsulates the business logic that is specific to
+The _**Model layer**_ represents the domain model (such as Account, Product,
+Person, Post, etc.) and encapsulates the business logic specific to
your application. In Rails, database-backed model classes are derived from
-`ActiveRecord::Base`. Active Record allows you to present the data from
+`ActiveRecord::Base`. [Active Record](activerecord/README.rdoc) allows you to present the data from
database rows as objects and embellish these data objects with business logic
-methods. You can read more about Active Record in its [README](activerecord/README.rdoc).
+methods.
Although most Rails models are backed by a database, models can also be ordinary
Ruby classes, or Ruby classes that implement a set of interfaces as provided by
-the Active Model module. You can read more about Active Model in its [README](activemodel/README.rdoc).
+the [Active Model](activemodel/README.rdoc) module.
+
+## Controller layer
-The _Controller layer_ is responsible for handling incoming HTTP requests and
+The _**Controller layer**_ is responsible for handling incoming HTTP requests and
providing a suitable response. Usually this means returning HTML, but Rails controllers
can also generate XML, JSON, PDFs, mobile-specific views, and more. Controllers load and
manipulate models, and render view templates in order to generate the appropriate HTTP response.
In Rails, incoming requests are routed by Action Dispatch to an appropriate controller, and
controller classes are derived from `ActionController::Base`. Action Dispatch and Action Controller
-are bundled together in Action Pack. You can read more about Action Pack in its
-[README](actionpack/README.rdoc).
+are bundled together in [Action Pack](actionpack/README.rdoc).
-The _View layer_ is composed of "templates" that are responsible for providing
+## View layer
+
+The _**View layer**_ is composed of "templates" that are responsible for providing
appropriate representations of your application's resources. Templates can
come in a variety of formats, but most view templates are HTML with embedded
Ruby code (ERB files). Views are typically rendered to generate a controller response,
-or to generate the body of an email. In Rails, View generation is handled by Action View.
-You can read more about Action View in its [README](actionview/README.rdoc).
+or to generate the body of an email. In Rails, View generation is handled by [Action View](actionview/README.rdoc).
+
+## Frameworks and libraries
-Active Record, Active Model, Action Pack, and Action View can each be used independently outside Rails.
-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
+[Active Record](activerecord/README.rdoc), [Active Model](activemodel/README.rdoc), [Action Pack](actionpack/README.rdoc), and [Action View](actionview/README.rdoc) can each be used independently outside Rails.
+In addition to that, Rails also comes with [Action Mailer](actionmailer/README.rdoc), a library
+to generate and send emails; [Active Job](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
-integrate WebSockets with a Rails application;
-Active Storage ([README](activestorage/README.md)), a library to attach cloud
+backends; [Action Cable](actioncable/README.md), a framework to
+integrate WebSockets with a Rails application; [Active Storage](activestorage/README.md), a library to attach cloud
and local files to Rails applications;
-and Active Support ([README](activesupport/README.rdoc)), a collection
+and [Active Support](activesupport/README.rdoc), a collection
of utility classes and standard library extensions that are useful for Rails,
and may also be used independently outside Rails.
@@ -65,7 +72,7 @@ and may also be used independently outside Rails.
Run with `--help` or `-h` for options.
-4. Using a browser, go to `http://localhost:3000` and you'll see:
+4. Go to `http://localhost:3000` and you'll see:
"Yay! You’re on Rails!"
5. Follow the guidelines to start developing your application. You may find
diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md
index 3ff28c29f5..60434451fe 100644
--- a/RELEASING_RAILS.md
+++ b/RELEASING_RAILS.md
@@ -14,14 +14,14 @@ Today is mostly coordination tasks. Here are the things you must do today:
Do not release with a Red CI. You can find the CI status here:
```
-http://travis-ci.org/rails/rails
+https://travis-ci.org/rails/rails
```
### Is Sam Ruby happy? If not, make him happy.
Sam Ruby keeps a [test suite](https://github.com/rubys/awdwr) that makes
sure the code samples in his book
-([Agile Web Development with Rails](https://pragprog.com/titles/rails5/agile-web-development-with-rails-5th-edition))
+([Agile Web Development with Rails](https://pragprog.com/book/rails51/agile-web-development-with-rails-51))
all work. These are valuable system tests
for Rails. You can check the status of these tests here:
@@ -141,7 +141,7 @@ lists where you should announce:
Use Markdown format for your announcement. Remember to ask people to report
issues with the release candidate to the rails-core mailing list.
-NOTE: For patch releases there's a `rake announce` task to generate the release
+NOTE: For patch releases, there's a `rake announce` task to generate the release
post. It supports multiple patch releases too:
```
diff --git a/actioncable/.gitignore b/actioncable/.gitignore
index 0a04b29786..f514e58c16 100644
--- a/actioncable/.gitignore
+++ b/actioncable/.gitignore
@@ -1,2 +1,2 @@
-/lib/assets/compiled
-/tmp
+/lib/assets/compiled/
+/tmp/
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md
index 560ee89846..959943016f 100644
--- a/actioncable/CHANGELOG.md
+++ b/actioncable/CHANGELOG.md
@@ -1,26 +1,6 @@
-* Hash long stream identifiers when using PostgreSQL adapter.
+* Rails 6 requires Ruby 2.4.1 or newer.
- PostgreSQL has a limit on identifiers length (63 chars, [docs](https://www.postgresql.org/docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS)).
- Provided fix minifies identifiers longer than 63 chars by hashing them with SHA1.
+ *Jeremy Daer*
- Fixes #28751.
- *Vladimir Dementyev*
-
-* Action Cable's `redis` adapter allows for other common redis-rb options (`host`, `port`, `db`, `password`) in cable.yml.
-
- Previously, it accepts only a [redis:// url](https://www.iana.org/assignments/uri-schemes/prov/redis) as an option.
- While we can add all of these options to the `url` itself, it is not explicitly documented. This alternative setup
- is shown as the first example in the [Redis rubygem](https://github.com/redis/redis-rb#getting-started), which
- makes this set of options as sensible as using just the `url`.
-
- *Marc Rendl Ignacio*
-
-* Action Cable socket errors are now logged to the console
-
- Previously any socket errors were ignored and this made it hard to diagnose socket issues (e.g. as discussed in #28362).
-
- *Edward Poot*
-
-
-Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actioncable/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actioncable/CHANGELOG.md) for previous changes.
diff --git a/actioncable/MIT-LICENSE b/actioncable/MIT-LICENSE
index 1a0e653b69..a42759f024 100644
--- a/actioncable/MIT-LICENSE
+++ b/actioncable/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2015-2017 Basecamp, LLC
+Copyright (c) 2015-2018 Basecamp, LLC
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actioncable/README.md b/actioncable/README.md
index 70b39ead57..a05ef1dd20 100644
--- a/actioncable/README.md
+++ b/actioncable/README.md
@@ -446,7 +446,7 @@ 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 Action Cable as example implementations.
+Action Cable provides a subscription adapter interface to process its pubsub internals. By default, asynchronous, inline, PostgreSQL, and 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).
@@ -558,7 +558,7 @@ API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec
index b5b98f1a6b..d946d0797f 100644
--- a/actioncable/actioncable.gemspec
+++ b/actioncable/actioncable.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "WebSocket framework for Rails."
s.description = "Structure many real-time application concerns into channels over a single WebSocket connection."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
@@ -28,5 +28,5 @@ Gem::Specification.new do |s|
s.add_dependency "actionpack", version
s.add_dependency "nio4r", "~> 2.0"
- s.add_dependency "websocket-driver", "~> 0.6.1"
+ s.add_dependency "websocket-driver", ">= 0.6.1"
end
diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb
index bd828b2d0f..e7456e3c1b 100644
--- a/actioncable/lib/action_cable.rb
+++ b/actioncable/lib/action_cable.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2015-2017 Basecamp, LLC
+# Copyright (c) 2015-2018 Basecamp, LLC
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -25,7 +25,7 @@
require "active_support"
require "active_support/rails"
-require_relative "action_cable/version"
+require "action_cable/version"
module ActionCable
extend ActiveSupport::Autoload
diff --git a/actioncable/lib/action_cable/channel/broadcasting.rb b/actioncable/lib/action_cable/channel/broadcasting.rb
index acc791817b..9a96720f4a 100644
--- a/actioncable/lib/action_cable/channel/broadcasting.rb
+++ b/actioncable/lib/action_cable/channel/broadcasting.rb
@@ -9,7 +9,7 @@ module ActionCable
delegate :broadcasting_for, to: :class
- class_methods do
+ module ClassMethods
# Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel.
def broadcast_to(model, message)
ActionCable.server.broadcast(broadcasting_for([ channel_name, model ]), message)
diff --git a/actioncable/lib/action_cable/channel/callbacks.rb b/actioncable/lib/action_cable/channel/callbacks.rb
index 4223c0d996..e4cb19b26a 100644
--- a/actioncable/lib/action_cable/channel/callbacks.rb
+++ b/actioncable/lib/action_cable/channel/callbacks.rb
@@ -13,7 +13,7 @@ module ActionCable
define_callbacks :unsubscribe
end
- class_methods do
+ module ClassMethods
def before_subscribe(*methods, &block)
set_callback(:subscribe, :before, *methods, &block)
end
diff --git a/actioncable/lib/action_cable/channel/naming.rb b/actioncable/lib/action_cable/channel/naming.rb
index 03a5dcd3a0..9c324a2a53 100644
--- a/actioncable/lib/action_cable/channel/naming.rb
+++ b/actioncable/lib/action_cable/channel/naming.rb
@@ -5,7 +5,7 @@ module ActionCable
module Naming
extend ActiveSupport::Concern
- class_methods do
+ module ClassMethods
# Returns the name of the channel, underscored, without the <tt>Channel</tt> ending.
# If the channel is in a namespace, then the namespaces are represented by single
# colon separators in the channel name.
diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb
index 84053db9fd..11a1f1a5e8 100644
--- a/actioncable/lib/action_cable/connection/base.rb
+++ b/actioncable/lib/action_cable/connection/base.rb
@@ -136,13 +136,10 @@ module ActionCable
send_async :handle_close
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ private
attr_reader :websocket
attr_reader :message_buffer
- private
# The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc.
def request # :doc:
@request ||= begin
diff --git a/actioncable/lib/action_cable/connection/client_socket.rb b/actioncable/lib/action_cable/connection/client_socket.rb
index ba33c8b982..4b1964c4ae 100644
--- a/actioncable/lib/action_cable/connection/client_socket.rb
+++ b/actioncable/lib/action_cable/connection/client_socket.rb
@@ -21,7 +21,7 @@ module ActionCable
return true if env["HTTP_X_FORWARDED_PROTO"] == "https"
return true if env["rack.url_scheme"] == "https"
- return false
+ false
end
CONNECTING = 0
@@ -83,7 +83,7 @@ module ActionCable
when Numeric then @driver.text(message.to_s)
when String then @driver.text(message)
when Array then @driver.binary(message)
- else false
+ else false
end
end
diff --git a/actioncable/lib/action_cable/connection/identification.rb b/actioncable/lib/action_cable/connection/identification.rb
index 4b5f9ca115..cc544685dd 100644
--- a/actioncable/lib/action_cable/connection/identification.rb
+++ b/actioncable/lib/action_cable/connection/identification.rb
@@ -11,7 +11,7 @@ module ActionCable
class_attribute :identifiers, default: Set.new
end
- class_methods do
+ module ClassMethods
# 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.
#
diff --git a/actioncable/lib/action_cable/connection/message_buffer.rb b/actioncable/lib/action_cable/connection/message_buffer.rb
index f151a47072..965841b67e 100644
--- a/actioncable/lib/action_cable/connection/message_buffer.rb
+++ b/actioncable/lib/action_cable/connection/message_buffer.rb
@@ -30,13 +30,10 @@ module ActionCable
receive_buffered_messages
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ private
attr_reader :connection
attr_reader :buffered_messages
- private
def valid?(message)
message.is_a?(String)
end
diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb
index faafd6d0a6..1ad8d05107 100644
--- a/actioncable/lib/action_cable/connection/subscriptions.rb
+++ b/actioncable/lib/action_cable/connection/subscriptions.rb
@@ -43,7 +43,7 @@ module ActionCable
def remove(data)
logger.info "Unsubscribing from channel: #{data['identifier']}"
- remove_subscription subscriptions[data["identifier"]]
+ remove_subscription find(data)
end
def remove_subscription(subscription)
@@ -63,12 +63,8 @@ module ActionCable
subscriptions.each { |id, channel| remove_subscription(channel) }
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
- attr_reader :connection, :subscriptions
-
private
+ attr_reader :connection, :subscriptions
delegate :logger, to: :connection
def find(data)
diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb
index 81233ace34..31f29fdd2f 100644
--- a/actioncable/lib/action_cable/connection/web_socket.rb
+++ b/actioncable/lib/action_cable/connection/web_socket.rb
@@ -34,9 +34,7 @@ module ActionCable
websocket.rack_response
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ private
attr_reader :websocket
end
end
diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb
index c961f47342..53cbb597cd 100644
--- a/actioncable/lib/action_cable/engine.rb
+++ b/actioncable/lib/action_cable/engine.rb
@@ -2,7 +2,7 @@
require "rails"
require "action_cable"
-require_relative "helpers/action_cable_helper"
+require "action_cable/helpers/action_cable_helper"
require "active_support/core_ext/hash/indifferent_access"
module ActionCable
diff --git a/actioncable/lib/action_cable/gem_version.rb b/actioncable/lib/action_cable/gem_version.rb
index af8277d06e..cd1d9bccef 100644
--- a/actioncable/lib/action_cable/gem_version.rb
+++ b/actioncable/lib/action_cable/gem_version.rb
@@ -7,8 +7,8 @@ module ActionCable
end
module VERSION
- MAJOR = 5
- MINOR = 2
+ MAJOR = 6
+ MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb
index 82fed81a18..26209537df 100644
--- a/actioncable/lib/action_cable/server/configuration.rb
+++ b/actioncable/lib/action_cable/server/configuration.rb
@@ -25,13 +25,26 @@ module ActionCable
# Also makes sure proper dependencies are required.
def pubsub_adapter
adapter = (cable.fetch("adapter") { "redis" })
+
+ # Require the adapter itself and give useful feedback about
+ # 1. Missing adapter gems and
+ # 2. Adapter gems' missing dependencies.
path_to_adapter = "action_cable/subscription_adapter/#{adapter}"
begin
require path_to_adapter
- rescue Gem::LoadError => e
- raise Gem::LoadError, "Specified '#{adapter}' for Action Cable pubsub adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by Action Cable)."
rescue LoadError => e
- raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/cable.yml is valid. If you use an adapter other than 'postgresql' or 'redis' add the necessary adapter gem to the Gemfile.", e.backtrace
+ # We couldn't require the adapter itself. Raise an exception that
+ # points out config typos and missing gems.
+ if e.path == path_to_adapter
+ # We can assume that a non-builtin adapter was specified, so it's
+ # either misspelled or missing from Gemfile.
+ raise e.class, "Could not load the '#{adapter}' Action Cable pubsub adapter. Ensure that the adapter is spelled correctly in config/cable.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
+
+ # Bubbled up from the adapter require. Prefix the exception message
+ # with some guidance about how to address it and reraise.
+ else
+ raise e.class, "Error loading the '#{adapter}' Action Cable pubsub adapter. Missing a gem it depends on? #{e.message}", e.backtrace
+ end
end
adapter = adapter.camelize
diff --git a/actioncable/lib/action_cable/subscription_adapter/async.rb b/actioncable/lib/action_cable/subscription_adapter/async.rb
index 96c18c4a2f..c9930299c7 100644
--- a/actioncable/lib/action_cable/subscription_adapter/async.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/async.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "inline"
+require "action_cable/subscription_adapter/inline"
module ActionCable
module SubscriptionAdapter
diff --git a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb
deleted file mode 100644
index 07774810ce..0000000000
--- a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-# frozen_string_literal: true
-
-require "thread"
-
-gem "em-hiredis", "~> 0.3.0"
-gem "redis", "~> 3.0"
-require "em-hiredis"
-require "redis"
-
-EventMachine.epoll if EventMachine.epoll?
-EventMachine.kqueue if EventMachine.kqueue?
-
-module ActionCable
- module SubscriptionAdapter
- class EventedRedis < Base # :nodoc:
- prepend ChannelPrefix
-
- @@mutex = Mutex.new
-
- # 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, default: ->(config) { EM::Hiredis.connect(config[:url]) }
-
- # 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, default: ->(config) { ::Redis.new(url: config[:url]) }
-
- def initialize(*)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- The "evented_redis" subscription adapter is deprecated and
- will be removed in Rails 5.2. Please use the "redis" adapter
- instead.
- MSG
-
- super
- @redis_connection_for_broadcasts = @redis_connection_for_subscriptions = nil
- end
-
- def broadcast(channel, payload)
- redis_connection_for_broadcasts.publish(channel, payload)
- end
-
- def subscribe(channel, message_callback, success_callback = nil)
- redis_connection_for_subscriptions.pubsub.subscribe(channel, &message_callback).tap do |result|
- result.callback { |reply| success_callback.call } if success_callback
- end
- end
-
- def unsubscribe(channel, message_callback)
- redis_connection_for_subscriptions.pubsub.unsubscribe_proc(channel, message_callback)
- end
-
- def shutdown
- redis_connection_for_subscriptions.pubsub.close_connection
- @redis_connection_for_subscriptions = nil
- end
-
- private
- def redis_connection_for_subscriptions
- ensure_reactor_running
- @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.error "[ActionCable] Redis reconnect failed."
- end
-
- redis.on(:failed) do
- @logger.error "[ActionCable] Redis connection has failed."
- end
- end
- end
- 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)
- end
- end
-
- def ensure_reactor_running
- return if EventMachine.reactor_running? && EventMachine.reactor_thread
- @@mutex.synchronize do
- Thread.new { EventMachine.run } unless EventMachine.reactor_running?
- Thread.pass until EventMachine.reactor_running? && EventMachine.reactor_thread
- end
- end
- end
- end
-end
diff --git a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
index a9c0949950..e384ea4afb 100644
--- a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-gem "pg", "~> 0.18"
+gem "pg", ">= 0.18", "< 2.0"
require "pg"
require "thread"
require "digest/sha1"
diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb
index facea944ff..c28951608f 100644
--- a/actioncable/lib/action_cable/subscription_adapter/redis.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb
@@ -2,7 +2,7 @@
require "thread"
-gem "redis", "~> 3.0"
+gem "redis", ">= 3", "< 5"
require "redis"
module ActionCable
@@ -76,7 +76,7 @@ module ActionCable
def listen(conn)
conn.without_reconnect do
- original_client = conn.client
+ original_client = conn.respond_to?(:_client) ? conn._client : conn.client
conn.subscribe("_action_cable_internal") do |on|
on.subscribe do |chan, count|
diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb
index c3528370c6..427eef1f55 100644
--- a/actioncable/lib/rails/generators/channel/channel_generator.rb
+++ b/actioncable/lib/rails/generators/channel/channel_generator.rb
@@ -27,7 +27,7 @@ module Rails
private
def file_name
- @_file_name ||= super.gsub(/_channel/i, "")
+ @_file_name ||= super.sub(/_channel\z/i, "")
end
# FIXME: Change these files to symlinks once RubyGems 2.5.0 is required.
diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb.tt
index d672697283..d672697283 100644
--- a/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb
+++ b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb.tt
diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb.tt
index 0ff5442f47..0ff5442f47 100644
--- a/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb
+++ b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb.tt
diff --git a/actioncable/lib/rails/generators/channel/templates/assets/cable.js b/actioncable/lib/rails/generators/channel/templates/assets/cable.js.tt
index 739aa5f022..739aa5f022 100644
--- a/actioncable/lib/rails/generators/channel/templates/assets/cable.js
+++ b/actioncable/lib/rails/generators/channel/templates/assets/cable.js.tt
diff --git a/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee b/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee.tt
index 5467811aba..5467811aba 100644
--- a/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee
+++ b/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee.tt
diff --git a/actioncable/lib/rails/generators/channel/templates/assets/channel.js b/actioncable/lib/rails/generators/channel/templates/assets/channel.js.tt
index ab0e68b11a..ab0e68b11a 100644
--- a/actioncable/lib/rails/generators/channel/templates/assets/channel.js
+++ b/actioncable/lib/rails/generators/channel/templates/assets/channel.js.tt
diff --git a/actioncable/lib/rails/generators/channel/templates/channel.rb b/actioncable/lib/rails/generators/channel/templates/channel.rb.tt
index 4bcfb2be4d..4bcfb2be4d 100644
--- a/actioncable/lib/rails/generators/channel/templates/channel.rb
+++ b/actioncable/lib/rails/generators/channel/templates/channel.rb.tt
diff --git a/actioncable/package.json b/actioncable/package.json
index acec1e2e9c..d9df46043d 100644
--- a/actioncable/package.json
+++ b/actioncable/package.json
@@ -1,6 +1,6 @@
{
"name": "actioncable",
- "version": "5.2.0-alpha",
+ "version": "6.0.0-alpha",
"description": "WebSocket framework for Ruby on Rails.",
"main": "lib/assets/compiled/action_cable.js",
"files": [
diff --git a/actioncable/test/channel/base_test.rb b/actioncable/test/channel/base_test.rb
index 866bd7c21b..3b8eb63975 100644
--- a/actioncable/test/channel/base_test.rb
+++ b/actioncable/test/channel/base_test.rb
@@ -97,12 +97,12 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
@channel.subscribe_to_channel
assert @channel.room
- assert @channel.subscribed?
+ assert_predicate @channel, :subscribed?
@channel.unsubscribe_from_channel
- assert ! @channel.room
- assert ! @channel.subscribed?
+ assert_not @channel.room
+ assert_not_predicate @channel, :subscribed?
end
test "connection identifiers" do
diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb
index eca06fe365..53dd7e5b77 100644
--- a/actioncable/test/channel/stream_test.rb
+++ b/actioncable/test/channel/stream_test.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "test_helper"
+require "active_support/testing/method_call_assertions"
require "stubs/test_connection"
require "stubs/room"
@@ -143,6 +144,8 @@ module ActionCable::StreamTests
end
class StreamFromTest < ActionCable::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
setup do
@server = TestServer.new(subscription_adapter: ActionCable::SubscriptionAdapter::Async)
@server.config.allowed_request_origins = %w( http://rubyonrails.com )
@@ -153,10 +156,11 @@ module ActionCable::StreamTests
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
- wait_for_executor connection.server.worker_pool.executor
+ assert_called(connection.websocket, :transmit) do
+ @server.broadcast "test_room_1", { foo: "bar" }, { coder: DummyEncoder }
+ wait_for_async
+ wait_for_executor connection.server.worker_pool.executor
+ end
end
end
@@ -167,7 +171,7 @@ module ActionCable::StreamTests
@server.broadcast "channel", {}
wait_for_async
- refute Thread.current[:ran_callback], "User callback was not run through the worker pool"
+ assert_not Thread.current[:ran_callback], "User callback was not run through the worker pool"
end
end
@@ -175,10 +179,10 @@ module ActionCable::StreamTests
run_in_eventmachine do
connection = open_connection
expected = { "identifier" => { "channel" => MultiChatChannel.name }.to_json, "type" => "confirm_subscription" }
- connection.websocket.expects(:transmit).with(expected.to_json)
- receive(connection, command: "subscribe", channel: MultiChatChannel.name, identifiers: {})
-
- wait_for_async
+ assert_called(connection.websocket, :transmit, [expected.to_json]) do
+ receive(connection, command: "subscribe", channel: MultiChatChannel.name, identifiers: {})
+ wait_for_async
+ end
end
end
@@ -192,15 +196,15 @@ module ActionCable::StreamTests
Connection.new(@server, env).tap do |connection|
connection.process
- assert connection.websocket.possible?
+ assert_predicate connection.websocket, :possible?
wait_for_async
- assert connection.websocket.alive?
+ assert_predicate connection.websocket, :alive?
end
end
def receive(connection, command:, identifiers:, channel: "ActionCable::StreamTests::ChatChannel")
- identifier = JSON.generate(channel: channel, **identifiers)
+ identifier = JSON.generate(identifiers.merge(channel: channel))
connection.dispatch_websocket_message JSON.generate(command: command, identifier: identifier)
wait_for_async
end
diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb
index 56b3ef143b..92fe59c803 100644
--- a/actioncable/test/client_test.rb
+++ b/actioncable/test/client_test.rb
@@ -7,6 +7,7 @@ require "websocket-client-simple"
require "json"
require "active_support/hash_with_indifferent_access"
+require "active_support/testing/method_call_assertions"
####
# 😷 Warning suppression 😷
@@ -27,6 +28,8 @@ WebSocket::Frame::Data.prepend Module.new {
####
class ClientTest < ActionCable::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
WAIT_WHEN_EXPECTING_EVENT = 2
WAIT_WHEN_NOT_EXPECTING_EVENT = 0.5
@@ -289,9 +292,10 @@ class ClientTest < ActionCable::TestCase
subscriptions = app.connections.first.subscriptions.send(:subscriptions)
assert_not_equal 0, subscriptions.size, "Missing EchoChannel subscription"
channel = subscriptions.first[1]
- channel.expects(:unsubscribed)
- c.close
- sleep 0.1 # Data takes a moment to process
+ assert_called(channel, :unsubscribed) do
+ c.close
+ sleep 0.1 # Data takes a moment to process
+ end
# All data is removed: No more connection or subscription information!
assert_equal(0, app.connections.count)
@@ -307,7 +311,7 @@ class ClientTest < ActionCable::TestCase
ActionCable.server.restart
c.wait_for_close
- assert c.closed?
+ assert_predicate c, :closed?
end
end
end
diff --git a/actioncable/test/connection/authorization_test.rb b/actioncable/test/connection/authorization_test.rb
index 7d039336b8..be41d510ff 100644
--- a/actioncable/test/connection/authorization_test.rb
+++ b/actioncable/test/connection/authorization_test.rb
@@ -1,9 +1,12 @@
# frozen_string_literal: true
require "test_helper"
+require "active_support/testing/method_call_assertions"
require "stubs/test_server"
class ActionCable::Connection::AuthorizationTest < ActionCable::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
class Connection < ActionCable::Connection::Base
attr_reader :websocket
@@ -25,9 +28,9 @@ class ActionCable::Connection::AuthorizationTest < ActionCable::TestCase
"HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.com"
connection = Connection.new(server, env)
- connection.websocket.expects(:close)
-
- connection.process
+ assert_called(connection.websocket, :close) do
+ connection.process
+ end
end
end
end
diff --git a/actioncable/test/connection/base_test.rb b/actioncable/test/connection/base_test.rb
index 99488e38c8..ed62e90b70 100644
--- a/actioncable/test/connection/base_test.rb
+++ b/actioncable/test/connection/base_test.rb
@@ -3,8 +3,11 @@
require "test_helper"
require "stubs/test_server"
require "active_support/core_ext/object/json"
+require "active_support/testing/method_call_assertions"
class ActionCable::Connection::BaseTest < ActionCable::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
class Connection < ActionCable::Connection::Base
attr_reader :websocket, :subscriptions, :message_buffer, :connected
@@ -39,10 +42,10 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
connection = open_connection
connection.process
- assert connection.websocket.possible?
+ assert_predicate connection.websocket, :possible?
wait_for_async
- assert connection.websocket.alive?
+ assert_predicate connection.websocket, :alive?
end
end
@@ -59,11 +62,12 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
run_in_eventmachine do
connection = open_connection
- connection.websocket.expects(:transmit).with({ type: "welcome" }.to_json)
- connection.message_buffer.expects(:process!)
-
- connection.process
- wait_for_async
+ assert_called_with(connection.websocket, :transmit, [{ type: "welcome" }.to_json]) do
+ assert_called(connection.message_buffer, :process!) do
+ connection.process
+ wait_for_async
+ end
+ end
assert_equal [ connection ], @server.connections
assert connection.connected
@@ -80,10 +84,11 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
connection.send :handle_open
assert connection.connected
- connection.subscriptions.expects(:unsubscribe_from_all)
- connection.send :handle_close
+ assert_called(connection.subscriptions, :unsubscribe_from_all) do
+ connection.send :handle_close
+ end
- assert ! connection.connected
+ assert_not connection.connected
assert_equal [], @server.connections
end
end
@@ -95,7 +100,7 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
statistics = connection.statistics
- assert statistics[:identifier].blank?
+ assert_predicate statistics[:identifier], :blank?
assert_kind_of Time, statistics[:started_at]
assert_equal [], statistics[:subscriptions]
end
@@ -106,8 +111,9 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
connection = open_connection
connection.process
- connection.websocket.expects(:close)
- connection.close
+ assert_called(connection.websocket, :close) do
+ connection.close
+ end
end
end
diff --git a/actioncable/test/connection/client_socket_test.rb b/actioncable/test/connection/client_socket_test.rb
index 2051216010..da72501c8e 100644
--- a/actioncable/test/connection/client_socket_test.rb
+++ b/actioncable/test/connection/client_socket_test.rb
@@ -2,8 +2,11 @@
require "test_helper"
require "stubs/test_server"
+require "active_support/testing/method_call_assertions"
class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
class Connection < ActionCable::Connection::Base
attr_reader :connected, :websocket, :errors
@@ -41,9 +44,10 @@ class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase
# 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_not_called(client, :client_gone) do
+ client.write("boo")
+ end
assert_equal %w[ foo ], connection.errors
end
end
@@ -67,9 +71,9 @@ class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase
env = Rack::MockRequest.env_for "/test",
"HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket",
"HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.com"
- io = \
+ io, client_io = \
begin
- Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM, 0).first
+ Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM, 0)
rescue
StringIO.new
end
@@ -77,6 +81,14 @@ class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase
Connection.new(@server, env).tap do |connection|
connection.process
+ if client_io
+ # Make sure server returns handshake response
+ Timeout.timeout(1) do
+ loop do
+ break if client_io.readline == "\r\n"
+ end
+ end
+ end
connection.send :handle_open
assert connection.connected
end
diff --git a/actioncable/test/connection/identifier_test.rb b/actioncable/test/connection/identifier_test.rb
index 6b6c8cd31a..de1ae1d5b9 100644
--- a/actioncable/test/connection/identifier_test.rb
+++ b/actioncable/test/connection/identifier_test.rb
@@ -1,10 +1,13 @@
# frozen_string_literal: true
require "test_helper"
+require "active_support/testing/method_call_assertions"
require "stubs/test_server"
require "stubs/user"
class ActionCable::Connection::IdentifierTest < ActionCable::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
class Connection < ActionCable::Connection::Base
identified_by :current_user
attr_reader :websocket
@@ -41,8 +44,9 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase
run_in_eventmachine do
open_connection_with_stubbed_pubsub
- @connection.websocket.expects(:close)
- @connection.process_internal_message "type" => "disconnect"
+ assert_called(@connection.websocket, :close) do
+ @connection.process_internal_message "type" => "disconnect"
+ end
end
end
@@ -50,8 +54,9 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase
run_in_eventmachine do
open_connection_with_stubbed_pubsub
- @connection.websocket.expects(:close).never
- @connection.process_internal_message "type" => "unknown"
+ assert_not_called(@connection.websocket, :close) do
+ @connection.process_internal_message "type" => "unknown"
+ end
end
end
diff --git a/actioncable/test/connection/stream_test.rb b/actioncable/test/connection/stream_test.rb
index b0419b0994..1e1466af31 100644
--- a/actioncable/test/connection/stream_test.rb
+++ b/actioncable/test/connection/stream_test.rb
@@ -1,9 +1,12 @@
# frozen_string_literal: true
require "test_helper"
+require "active_support/testing/method_call_assertions"
require "stubs/test_server"
class ActionCable::Connection::StreamTest < ActionCable::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
class Connection < ActionCable::Connection::Base
attr_reader :connected, :websocket, :errors
@@ -42,9 +45,10 @@ class ActionCable::Connection::StreamTest < ActionCable::TestCase
# 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_called(client, :client_gone) do
+ client.write("boo")
+ end
assert_equal [], connection.errors
end
end
diff --git a/actioncable/test/connection/subscriptions_test.rb b/actioncable/test/connection/subscriptions_test.rb
index 149a40604a..7bc8c4241c 100644
--- a/actioncable/test/connection/subscriptions_test.rb
+++ b/actioncable/test/connection/subscriptions_test.rb
@@ -1,8 +1,11 @@
# frozen_string_literal: true
require "test_helper"
+require "active_support/testing/method_call_assertions"
class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
class Connection < ActionCable::Connection::Base
attr_reader :websocket
@@ -45,7 +48,7 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
setup_connection
@subscriptions.execute_command "command" => "subscribe"
- assert @subscriptions.identifiers.empty?
+ assert_empty @subscriptions.identifiers
end
end
@@ -55,10 +58,12 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
subscribe_to_chat_channel
channel = subscribe_to_chat_channel
- channel.expects(:unsubscribe_from_channel)
- @subscriptions.execute_command "command" => "unsubscribe", "identifier" => @chat_identifier
- assert @subscriptions.identifiers.empty?
+ assert_called(channel, :unsubscribe_from_channel) do
+ @subscriptions.execute_command "command" => "unsubscribe", "identifier" => @chat_identifier
+ end
+
+ assert_empty @subscriptions.identifiers
end
end
@@ -67,7 +72,7 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
setup_connection
@subscriptions.execute_command "command" => "unsubscribe"
- assert @subscriptions.identifiers.empty?
+ assert_empty @subscriptions.identifiers
end
end
@@ -92,10 +97,11 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
channel2_id = ActiveSupport::JSON.encode(id: 2, channel: "ActionCable::Connection::SubscriptionsTest::ChatChannel")
channel2 = subscribe_to_chat_channel(channel2_id)
- channel1.expects(:unsubscribe_from_channel)
- channel2.expects(:unsubscribe_from_channel)
-
- @subscriptions.unsubscribe_from_all
+ assert_called(channel1, :unsubscribe_from_channel) do
+ assert_called(channel2, :unsubscribe_from_channel) do
+ @subscriptions.unsubscribe_from_all
+ end
+ end
end
end
diff --git a/actioncable/test/server/base_test.rb b/actioncable/test/server/base_test.rb
index 1312e45f49..3b5931f0a4 100644
--- a/actioncable/test/server/base_test.rb
+++ b/actioncable/test/server/base_test.rb
@@ -3,8 +3,11 @@
require "test_helper"
require "stubs/test_server"
require "active_support/core_ext/hash/indifferent_access"
+require "active_support/testing/method_call_assertions"
class BaseTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
def setup
@server = ActionCable::Server::Base.new
@server.config.cable = { adapter: "async" }.with_indifferent_access
@@ -19,17 +22,20 @@ class BaseTest < ActiveSupport::TestCase
conn = FakeConnection.new
@server.add_connection(conn)
- conn.expects(:close)
- @server.restart
+ assert_called(conn, :close) do
+ @server.restart
+ end
end
test "#restart shuts down worker pool" do
- @server.worker_pool.expects(:halt)
- @server.restart
+ assert_called(@server.worker_pool, :halt) do
+ @server.restart
+ end
end
test "#restart shuts down pub/sub adapter" do
- @server.pubsub.expects(:shutdown)
- @server.restart
+ assert_called(@server.pubsub, :shutdown) do
+ @server.restart
+ end
end
end
diff --git a/actioncable/test/subscription_adapter/common.rb b/actioncable/test/subscription_adapter/common.rb
index c533a9f3eb..b3e9ae9d5c 100644
--- a/actioncable/test/subscription_adapter/common.rb
+++ b/actioncable/test/subscription_adapter/common.rb
@@ -32,7 +32,7 @@ module CommonSubscriptionAdapterTest
subscribed = Concurrent::Event.new
adapter.subscribe(channel, callback, Proc.new { subscribed.set })
subscribed.wait(WAIT_WHEN_EXPECTING_EVENT)
- assert subscribed.set?
+ assert_predicate subscribed, :set?
yield queue
diff --git a/actioncable/test/subscription_adapter/evented_redis_test.rb b/actioncable/test/subscription_adapter/evented_redis_test.rb
deleted file mode 100644
index e3e0a0c72a..0000000000
--- a/actioncable/test/subscription_adapter/evented_redis_test.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require_relative "common"
-require_relative "channel_prefix"
-
-class EventedRedisAdapterTest < ActionCable::TestCase
- include CommonSubscriptionAdapterTest
- include ChannelPrefixTest
-
- def setup
- assert_deprecated do
- super
- end
-
- # em-hiredis is warning-rich
- @previous_verbose, $VERBOSE = $VERBOSE, nil
- end
-
- def teardown
- super
-
- # Ensure EM is shut down before we re-enable warnings
- EventMachine.reactor_thread.tap do |thread|
- EventMachine.stop
- thread.join
- end
-
- $VERBOSE = @previous_verbose
- end
-
- def test_slow_eventmachine
- require "eventmachine"
- require "thread"
-
- lock = Mutex.new
-
- EventMachine.singleton_class.class_eval do
- alias_method :delayed_initialize_event_machine, :initialize_event_machine
- define_method(:initialize_event_machine) do
- lock.synchronize do
- sleep 0.5
- delayed_initialize_event_machine
- end
- end
- end
-
- test_basic_broadcast
- ensure
- lock.synchronize do
- EventMachine.singleton_class.class_eval do
- alias_method :initialize_event_machine, :delayed_initialize_event_machine
- remove_method :delayed_initialize_event_machine
- end
- end
- end
-
- def cable_config
- { adapter: "evented_redis", url: "redis://:password@127.0.0.1:6379/12" }
- end
-end
diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb
index 69120d5753..63823d6ef0 100644
--- a/actioncable/test/subscription_adapter/redis_test.rb
+++ b/actioncable/test/subscription_adapter/redis_test.rb
@@ -4,12 +4,15 @@ require "test_helper"
require_relative "common"
require_relative "channel_prefix"
+require "active_support/testing/method_call_assertions"
+require "action_cable/subscription_adapter/redis"
+
class RedisAdapterTest < ActionCable::TestCase
include CommonSubscriptionAdapterTest
include ChannelPrefixTest
def cable_config
- { adapter: "redis", driver: "ruby", url: "redis://:password@127.0.0.1:6379/12" }
+ { adapter: "redis", driver: "ruby" }
end
end
@@ -23,6 +26,22 @@ class RedisAdapterTest::AlternateConfiguration < RedisAdapterTest
def cable_config
alt_cable_config = super.dup
alt_cable_config.delete(:url)
- alt_cable_config.merge(host: "127.0.0.1", port: 6379, db: 12, password: "password")
+ alt_cable_config.merge(host: "127.0.0.1", port: 6379, db: 12)
+ end
+end
+
+class RedisAdapterTest::Connector < ActiveSupport::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
+ test "slices url, host, port, db, and password from config" do
+ config = { url: 1, host: 2, port: 3, db: 4, password: 5 }
+
+ assert_called_with ::Redis, :new, [ config ] do
+ connect config.merge(other: "unrelated", stuff: "here")
+ end
+ end
+
+ def connect(config)
+ ActionCable::SubscriptionAdapter::Redis.redis_connector.call(config)
end
end
diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb
index 2a4611fb37..2f186b7193 100644
--- a/actioncable/test/test_helper.rb
+++ b/actioncable/test/test_helper.rb
@@ -4,7 +4,7 @@ require "action_cable"
require "active_support/testing/autorun"
require "puma"
-require "mocha/setup"
+require "mocha/minitest"
require "rack/mock"
begin
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md
index 9993a11c9d..9fb2e44210 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,12 +1,10 @@
-* Allow Action Mailer classes to configure their delivery job.
+* Perform email jobs in `assert_emails`.
- class MyMailer < ApplicationMailer
- self.delivery_job = MyCustomDeliveryJob
+ *Gannon McGibbon*
- ...
- end
+* Rails 6 requires Ruby 2.4.1 or newer.
- *Matthew Mongeau*
+ *Jeremy Daer*
-Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionmailer/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actionmailer/CHANGELOG.md) for previous changes.
diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE
index ac810e86d0..1cb3add0fc 100644
--- a/actionmailer/MIT-LICENSE
+++ b/actionmailer/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2017 David Heinemeier Hansson
+Copyright (c) 2004-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc
index 9993d3777d..14dfb82234 100644
--- a/actionmailer/README.rdoc
+++ b/actionmailer/README.rdoc
@@ -148,7 +148,7 @@ The latest version of Action Mailer can be installed with RubyGems:
$ gem install actionmailer
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/actionmailer
@@ -166,7 +166,7 @@ API documentation is at
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec
index b8a2e80bd3..f2fb160bdd 100644
--- a/actionmailer/actionmailer.gemspec
+++ b/actionmailer/actionmailer.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Email composition, delivery, and receiving framework (part of Rails)."
s.description = "Email on Rails. Compose, deliver, receive, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb
index a170eb7917..fabbdd1b25 100644
--- a/actionmailer/lib/action_mailer.rb
+++ b/actionmailer/lib/action_mailer.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2004-2017 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -24,7 +24,7 @@
#++
require "abstract_controller"
-require_relative "action_mailer/version"
+require "action_mailer/version"
# Common Active Support usage in Action Mailer
require "active_support"
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index 1c1e1a9a3b..3af95081ee 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
require "mail"
-require_relative "collector"
+require "action_mailer/collector"
require "active_support/core_ext/string/inflections"
require "active_support/core_ext/hash/except"
require "active_support/core_ext/module/anonymous"
-require_relative "log_subscriber"
-require_relative "rescuable"
+require "action_mailer/log_subscriber"
+require "action_mailer/rescuable"
module ActionMailer
# Action Mailer allows you to send email from your application using a mailer model and views.
@@ -889,7 +889,7 @@ module ActionMailer
default_values = self.class.default.map do |key, value|
[
key,
- value.is_a?(Proc) ? instance_exec(&value) : value
+ compute_default(value)
]
end.to_h
@@ -898,6 +898,16 @@ module ActionMailer
headers_with_defaults
end
+ def compute_default(value)
+ return value unless value.is_a?(Proc)
+
+ if value.arity == 1
+ instance_exec(self, &value)
+ else
+ instance_exec(&value)
+ end
+ end
+
def assign_headers_to_message(message, headers)
assignable = headers.except(:parts_order, :content_type, :body, :template_name,
:template_path, :delivery_method, :delivery_method_options)
diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb
index 063d4580d8..72eb5d61e8 100644
--- a/actionmailer/lib/action_mailer/gem_version.rb
+++ b/actionmailer/lib/action_mailer/gem_version.rb
@@ -7,8 +7,8 @@ module ActionMailer
end
module VERSION
- MAJOR = 5
- MINOR = 2
+ MAJOR = 6
+ MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
index 4bef4a58d3..8a12f805cc 100644
--- a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
+++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
@@ -4,7 +4,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
+ # that use inline cid: style URLs to data: style URLs so that they are visible
# when previewing an HTML email in a web browser.
#
# This interceptor is enabled by default. To disable it, delete it from the
diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb
index a2ea45dc7b..2377aeb9a5 100644
--- a/actionmailer/lib/action_mailer/message_delivery.rb
+++ b/actionmailer/lib/action_mailer/message_delivery.rb
@@ -53,6 +53,12 @@ module ActionMailer
# Notifier.welcome(User.first).deliver_later!(wait: 1.hour)
# Notifier.welcome(User.first).deliver_later!(wait_until: 10.hours.from_now)
#
+ # Options:
+ #
+ # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay
+ # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time
+ # * <tt>:queue</tt> - Enqueue the email on the specified queue
+ #
# By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each
# <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable
# +delivery_job+.
@@ -60,12 +66,6 @@ module ActionMailer
# class AccountRegistrationMailer < ApplicationMailer
# self.delivery_job = RegistrationDeliveryJob
# end
- #
- # Options:
- #
- # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay
- # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time
- # * <tt>:queue</tt> - Enqueue the email on the specified queue
def deliver_later!(options = {})
enqueue_delivery :deliver_now!, options
end
@@ -77,6 +77,12 @@ module ActionMailer
# Notifier.welcome(User.first).deliver_later(wait: 1.hour)
# Notifier.welcome(User.first).deliver_later(wait_until: 10.hours.from_now)
#
+ # Options:
+ #
+ # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay.
+ # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time.
+ # * <tt>:queue</tt> - Enqueue the email on the specified queue.
+ #
# By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each
# <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable
# +delivery_job+.
@@ -84,12 +90,6 @@ module ActionMailer
# class AccountRegistrationMailer < ApplicationMailer
# self.delivery_job = RegistrationDeliveryJob
# end
- #
- # Options:
- #
- # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay.
- # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time.
- # * <tt>:queue</tt> - Enqueue the email on the specified queue.
def deliver_later(options = {})
enqueue_delivery :deliver_now, options
end
diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb
index 730ac89c94..0aea84fd2b 100644
--- a/actionmailer/lib/action_mailer/preview.rb
+++ b/actionmailer/lib/action_mailer/preview.rb
@@ -104,7 +104,7 @@ module ActionMailer
private
def load_previews
if preview_path
- Dir["#{preview_path}/**/*_preview.rb"].each { |file| require_dependency file }
+ Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
end
end
diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb
index ac8b944743..6906472660 100644
--- a/actionmailer/lib/action_mailer/test_helper.rb
+++ b/actionmailer/lib/action_mailer/test_helper.rb
@@ -28,13 +28,13 @@ module ActionMailer
#
# assert_emails 2 do
# ContactMailer.welcome.deliver_now
- # ContactMailer.welcome.deliver_now
+ # ContactMailer.welcome.deliver_later
# end
# end
- def assert_emails(number)
+ def assert_emails(number, &block)
if block_given?
original_count = ActionMailer::Base.deliveries.size
- yield
+ perform_enqueued_jobs(only: [ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob], &block)
new_count = ActionMailer::Base.deliveries.size
assert_equal number, new_count - original_count, "#{number} emails expected, but #{new_count - original_count} were sent"
else
@@ -93,6 +93,48 @@ module ActionMailer
assert_enqueued_jobs number, only: [ ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob ], &block
end
+ # Asserts that a specific email has been enqueued, optionally
+ # matching arguments.
+ #
+ # def test_email
+ # ContactMailer.welcome.deliver_later
+ # assert_enqueued_email_with ContactMailer, :welcome
+ # end
+ #
+ # def test_email_with_arguments
+ # ContactMailer.welcome("Hello", "Goodbye").deliver_later
+ # assert_enqueued_email_with ContactMailer, :welcome, args: ["Hello", "Goodbye"]
+ # end
+ #
+ # If a block is passed, that block should cause the specified email
+ # to be enqueued.
+ #
+ # def test_email_in_block
+ # assert_enqueued_email_with ContactMailer, :welcome do
+ # ContactMailer.welcome.deliver_later
+ # end
+ # end
+ #
+ # If +args+ is provided as a Hash, a parameterized email is matched.
+ #
+ # def test_parameterized_email
+ # assert_enqueued_email_with ContactMailer, :welcome,
+ # args: {email: 'user@example.com'} do
+ # ContactMailer.with(email: 'user@example.com').welcome.deliver_later
+ # end
+ # end
+ def assert_enqueued_email_with(mailer, method, args: nil, queue: "mailers", &block)
+ if args.is_a? Hash
+ job = ActionMailer::Parameterized::DeliveryJob
+ args = [mailer.to_s, method.to_s, "deliver_now", args]
+ else
+ job = ActionMailer::DeliveryJob
+ args = [mailer.to_s, method.to_s, "deliver_now", *args]
+ end
+
+ assert_enqueued_with(job: job, args: args, queue: queue, &block)
+ end
+
# Asserts that no emails are enqueued for later delivery.
#
# def test_no_emails
diff --git a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
index 97eac30db1..c37a74c762 100644
--- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
+++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
@@ -23,7 +23,7 @@ module Rails
private
def file_name # :doc:
- @_file_name ||= super.gsub(/_mailer/i, "")
+ @_file_name ||= super.sub(/_mailer\z/i, "")
end
def application_mailer_file_name
diff --git a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb.tt
index 00fb9bd48f..00fb9bd48f 100644
--- a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb
+++ b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb.tt
diff --git a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt
index 348d314758..348d314758 100644
--- a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb
+++ b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index 0da969c349..4124aa00bd 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -322,22 +322,21 @@ class BaseTest < ActiveSupport::TestCase
test "implicit multipart with attachments creates nested parts" do
email = BaseMailer.implicit_multipart(attachments: true)
- assert_equal("application/pdf", email.parts[0].mime_type)
- assert_equal("multipart/alternative", email.parts[1].mime_type)
- assert_equal("text/plain", email.parts[1].parts[0].mime_type)
- assert_equal("TEXT Implicit Multipart", email.parts[1].parts[0].body.encoded)
- assert_equal("text/html", email.parts[1].parts[1].mime_type)
- assert_equal("HTML Implicit Multipart", email.parts[1].parts[1].body.encoded)
+ assert_equal(%w[ application/pdf multipart/alternative ], email.parts.map(&:mime_type).sort)
+ multipart = email.parts.detect { |p| p.mime_type == "multipart/alternative" }
+ assert_equal("text/plain", multipart.parts[0].mime_type)
+ assert_equal("TEXT Implicit Multipart", multipart.parts[0].body.encoded)
+ assert_equal("text/html", multipart.parts[1].mime_type)
+ assert_equal("HTML Implicit Multipart", multipart.parts[1].body.encoded)
end
test "implicit multipart with attachments and sort order" do
order = ["text/html", "text/plain"]
with_default BaseMailer, parts_order: order do
email = BaseMailer.implicit_multipart(attachments: true)
- assert_equal("application/pdf", email.parts[0].mime_type)
- assert_equal("multipart/alternative", email.parts[1].mime_type)
- assert_equal("text/plain", email.parts[1].parts[1].mime_type)
- assert_equal("text/html", email.parts[1].parts[0].mime_type)
+ assert_equal(%w[ application/pdf multipart/alternative ], email.parts.map(&:mime_type).sort)
+ multipart = email.parts.detect { |p| p.mime_type == "multipart/alternative" }
+ assert_equal(%w[ text/html text/plain ], multipart.parts.map(&:mime_type).sort)
end
end
@@ -427,12 +426,12 @@ class BaseTest < ActiveSupport::TestCase
test "explicit multipart with attachments creates nested parts" do
email = BaseMailer.explicit_multipart(attachments: true)
- assert_equal("application/pdf", email.parts[0].mime_type)
- assert_equal("multipart/alternative", email.parts[1].mime_type)
- assert_equal("text/plain", email.parts[1].parts[0].mime_type)
- assert_equal("TEXT Explicit Multipart", email.parts[1].parts[0].body.encoded)
- assert_equal("text/html", email.parts[1].parts[1].mime_type)
- assert_equal("HTML Explicit Multipart", email.parts[1].parts[1].body.encoded)
+ assert_equal(%w[ application/pdf multipart/alternative ], email.parts.map(&:mime_type).sort)
+ multipart = email.parts.detect { |p| p.mime_type == "multipart/alternative" }
+ assert_equal("text/plain", multipart.parts[0].mime_type)
+ assert_equal("TEXT Explicit Multipart", multipart.parts[0].body.encoded)
+ assert_equal("text/html", multipart.parts[1].mime_type)
+ assert_equal("HTML Explicit Multipart", multipart.parts[1].body.encoded)
end
test "explicit multipart with templates" do
@@ -507,8 +506,8 @@ class BaseTest < ActiveSupport::TestCase
test "should respond to action methods" do
assert_respond_to BaseMailer, :welcome
assert_respond_to BaseMailer, :implicit_multipart
- assert !BaseMailer.respond_to?(:mail)
- assert !BaseMailer.respond_to?(:headers)
+ assert_not_respond_to BaseMailer, :mail
+ assert_not_respond_to BaseMailer, :headers
end
test "calling just the action should return the generated mail object" do
@@ -726,6 +725,15 @@ class BaseTest < ActiveSupport::TestCase
assert(ProcMailer.welcome["x-has-to-proc"].to_s == "symbol")
end
+ test "proc default values can have arity of 1 where arg is a mailer instance" do
+ assert_equal(ProcMailer.welcome["X-Lambda-Arity-1-arg"].to_s, "complex_value")
+ assert_equal(ProcMailer.welcome["X-Lambda-Arity-1-self"].to_s, "complex_value")
+ end
+
+ test "proc default values with fixed arity of 0 can be called" do
+ assert_equal("0", ProcMailer.welcome["X-Lambda-Arity-0"].to_s)
+ end
+
test "we can call other defined methods on the class as needed" do
mail = ProcMailer.welcome
assert_equal("Thanks for signing up this afternoon", mail.subject)
diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb
index e11e8d4676..22f310f39f 100644
--- a/actionmailer/test/caching_test.rb
+++ b/actionmailer/test/caching_test.rb
@@ -40,14 +40,14 @@ class FragmentCachingTest < BaseCachingTest
def test_fragment_exist_with_caching_enabled
@store.write("views/name", "value")
assert @mailer.fragment_exist?("name")
- assert !@mailer.fragment_exist?("other_name")
+ assert_not @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")
+ assert_not @mailer.fragment_exist?("name")
+ assert_not @mailer.fragment_exist?("other_name")
end
def test_write_fragment_with_caching_enabled
@@ -90,7 +90,7 @@ class FragmentCachingTest < BaseCachingTest
buffer = "generated till now -> ".html_safe
buffer << view_context.send(:fragment_for, "expensive") { fragment_computed = true }
- assert !fragment_computed
+ assert_not fragment_computed
assert_equal "generated till now -> fragment content", buffer
end
@@ -105,7 +105,7 @@ class FragmentCachingTest < BaseCachingTest
html_safe = @mailer.read_fragment("name")
assert_equal content, html_safe
- assert html_safe.html_safe?
+ assert_predicate html_safe, :html_safe?
end
end
@@ -198,7 +198,7 @@ end
class CacheHelperOutputBufferTest < BaseCachingTest
class MockController
def read_fragment(name, options)
- return false
+ false
end
def write_fragment(name, fragment, options)
@@ -214,9 +214,9 @@ class CacheHelperOutputBufferTest < BaseCachingTest
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;
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
end
cache_helper.extend(ActionView::Helpers::CacheHelper)
@@ -235,9 +235,9 @@ class CacheHelperOutputBufferTest < BaseCachingTest
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;
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
end
cache_helper.extend(ActionView::Helpers::CacheHelper)
@@ -262,7 +262,7 @@ class ViewCacheDependencyTest < BaseCachingTest
end
def test_view_cache_dependencies_are_empty_by_default
- assert NoDependenciesMailer.new.view_cache_dependencies.empty?
+ assert_empty NoDependenciesMailer.new.view_cache_dependencies
end
def test_view_cache_dependencies_are_listed_in_declaration_order
diff --git a/actionmailer/test/mailers/proc_mailer.rb b/actionmailer/test/mailers/proc_mailer.rb
index b7cf53eb4a..76e730bb79 100644
--- a/actionmailer/test/mailers/proc_mailer.rb
+++ b/actionmailer/test/mailers/proc_mailer.rb
@@ -4,12 +4,19 @@ class ProcMailer < ActionMailer::Base
default to: "system@test.lindsaar.net",
"X-Proc-Method" => Proc.new { Time.now.to_i.to_s },
subject: Proc.new { give_a_greeting },
- "x-has-to-proc" => :symbol
+ "x-has-to-proc" => :symbol,
+ "X-Lambda-Arity-0" => ->() { "0" },
+ "X-Lambda-Arity-1-arg" => ->(arg) { arg.computed_value },
+ "X-Lambda-Arity-1-self" => ->(_) { self.computed_value }
def welcome
mail
end
+ def computed_value
+ "complex_value"
+ end
+
private
def give_a_greeting
diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb
index 03e8d4fb66..f8dcb3f4ba 100644
--- a/actionmailer/test/message_delivery_test.rb
+++ b/actionmailer/test/message_delivery_test.rb
@@ -39,7 +39,7 @@ class MessageDeliveryTest < ActiveSupport::TestCase
end
test "its message should be a Mail::Message" do
- assert_equal Mail::Message , @mail.message.class
+ assert_equal Mail::Message, @mail.message.class
end
test "should respond to .deliver_later" do
diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb
index abf50cf4da..8fdc687a8b 100644
--- a/actionmailer/test/test_helper_test.rb
+++ b/actionmailer/test/test_helper_test.rb
@@ -10,6 +10,18 @@ class TestHelperMailer < ActionMailer::Base
to: "test@example.com",
from: "tester@example.com"
end
+
+ def test_args(recipient, name)
+ mail body: render(inline: "Hello, #{name}"),
+ to: recipient,
+ from: "tester@example.com"
+ end
+
+ def test_parameter_args
+ mail body: render(inline: "All is #{params[:all]}"),
+ to: "test@example.com",
+ from: "tester@example.com"
+ end
end
class TestHelperMailerTest < ActionMailer::TestCase
@@ -42,7 +54,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
def test_encode
- assert_equal "=?UTF-8?Q?This_is_=E3=81=82_string?=", encode("This is あ string")
+ assert_equal "This is あ string", Mail::Encodings.q_value_decode(encode("This is あ string"))
end
def test_read_fixture
@@ -57,6 +69,16 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
end
+ def test_assert_emails_with_enqueued_emails
+ assert_nothing_raised do
+ assert_emails 1 do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ end
+ end
+ end
+ end
+
def test_repeated_assert_emails_calls
assert_nothing_raised do
assert_emails 1 do
@@ -93,6 +115,18 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
end
+ def test_assert_no_emails_with_enqueued_emails
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_emails do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ end
+ end
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
def test_assert_emails_too_few_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_emails 2 do
@@ -207,6 +241,36 @@ class TestHelperMailerTest < ActionMailer::TestCase
assert_match(/0 .* but 1/, error.message)
end
+
+ def test_assert_enqueued_email_with
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer, :test do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ end
+ end
+ end
+ end
+
+ def test_assert_enqueued_email_with_args
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer, :test_args, args: ["some_email", "some_name"] do
+ silence_stream($stdout) do
+ TestHelperMailer.test_args("some_email", "some_name").deliver_later
+ end
+ end
+ end
+ end
+
+ def test_assert_enqueued_email_with_parameterized_args
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer, :test_parameter_args, args: { all: "good" } do
+ silence_stream($stdout) do
+ TestHelperMailer.with(all: "good").test_parameter_args.deliver_later
+ end
+ end
+ end
+ end
end
class AnotherTestHelperMailerTest < ActionMailer::TestCase
diff --git a/actionmailer/test/url_test.rb b/actionmailer/test/url_test.rb
index 82035689ad..3c940bc969 100644
--- a/actionmailer/test/url_test.rb
+++ b/actionmailer/test/url_test.rb
@@ -105,7 +105,7 @@ class ActionMailerUrlTest < ActionMailer::TestCase
assert_url_for "/dummy_model", DummyModel
# array
- assert_url_for "/dummy_model" , [DummyModel]
+ assert_url_for "/dummy_model", [DummyModel]
end
def test_signed_up_with_url
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 932968fa35..a13d9e1078 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,96 +1,44 @@
-* Cookies `:expires` option supports `ActiveSupport::Duration` object.
+* Introduce a new error page to when the implict render page is accessed in the browser.
- cookies[:user_name] = { value: "assain", expires: 1.hour }
- cookies[:key] = { value: "a yummy cookie", expires: 6.months }
+ Now instead of showing an error page that with exception and backtraces we now show only
+ one informative page.
- Pull Request: #30121
+ *Vinicius Stock*
- *Assain Jaleel*
+* Introduce ActionDispatch::DebugExceptions.register_interceptor
-* Enforce signed/encrypted cookie expiry server side.
+ Exception aware plugin authors can use the newly introduced
+ `.register_interceptor` method to get the processed exception, instead of
+ monkey patching DebugExceptions.
- Rails can thwart attacks by malicious clients that don't honor a cookie's expiry.
+ ActionDispatch::DebugExceptions.register_interceptor do |request, exception|
+ HypoteticalPlugin.capture_exception(request, exception)
+ end
- It does so by stashing the expiry within the written cookie and relying on the
- signing/encrypting to vouch that it hasn't been tampered with. Then on a
- server-side read, the expiry is verified and any expired cookie is discarded.
+ *Genadi Samokovarov*
- Pull Request: #30121
+* Output only one Content-Security-Policy nonce header value per request.
- *Assain Jaleel*
+ Fixes #32597.
-* Make `take_failed_screenshot` work within engine.
+ *Andrey Novikov*, *Andrew White*
- Fixes #30405.
+* Move default headers configuration into their own module that can be included in controllers.
- *Yuji Yaginuma*
+ *Kevin Deisz*
-* Deprecate `ActionDispatch::TestResponse` response aliases
+* Add method `dig` to `session`.
- `#success?`, `#missing?` & `#error?` are not supported by the actual
- `ActionDispatch::Response` object and can produce false-positives. Instead,
- use the response helpers provided by `Rack::Response`.
+ *claudiob*, *Takumi Shotoku*
- *Trevor Wistaff*
+* Controller level `force_ssl` has been deprecated in favor of
+ `config.force_ssl`.
-* Protect from forgery by default
+ *Derek Prior*
- Rather than protecting from forgery in the generated `ApplicationController`,
- add it to `ActionController::Base` depending on
- `config.action_controller.default_protect_from_forgery`. This configuration
- defaults to false to support older versions which have removed it from their
- `ApplicationController`, but is set to true for Rails 5.2.
+* Rails 6 requires Ruby 2.4.1 or newer.
- *Lisa Ugray*
+ *Jeremy Daer*
-* Fallback `ActionController::Parameters#to_s` to `Hash#to_s`.
- *Kir Shatrov*
-
-* `driven_by` now registers poltergeist and capybara-webkit
-
- If poltergeist or capybara-webkit are set as drivers is set for System Tests,
- `driven_by` will register the driver and set additional options passed via
- the `:options` parameter.
-
- Refer to the respective driver's documentation to see what options can be passed.
-
- *Mario Chavez*
-
-* AEAD encrypted cookies and sessions with GCM
-
- Encrypted cookies now use AES-GCM which couples authentication and
- encryption in one faster step and produces shorter ciphertexts. Cookies
- encrypted using AES in CBC HMAC mode will be seamlessly upgraded when
- this new mode is enabled via the
- `action_dispatch.use_authenticated_cookie_encryption` configuration value.
-
- *Michael J Coyne*
-
-* Change the cache key format for fragments to make it easier to debug key churn. The new format is:
-
- views/template/action.html.erb:7a1156131a6928cb0026877f8b749ac9/projects/123
- ^template path ^template tree digest ^class ^id
-
- *DHH*
-
-* Add support for recyclable cache keys with fragment caching. This uses the new versioned entries in the
- `ActiveSupport::Cache` stores and relies on the fact that Active Record has split `#cache_key` and `#cache_version`
- to support it.
-
- *DHH*
-
-* Add `action_controller_api` and `action_controller_base` load hooks to be called in `ActiveSupport.on_load`
-
- `ActionController::Base` and `ActionController::API` have differing implementations. This means that
- the one umbrella hook `action_controller` is not able to address certain situations where a method
- may not exist in a certain implementation.
-
- This is fixed by adding two new hooks so you can target `ActionController::Base` vs `ActionController::API`
-
- Fixes #27013.
-
- *Julian Nadeau*
-
-
-Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionpack/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actionpack/CHANGELOG.md) for previous changes.
diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE
index ac810e86d0..1cb3add0fc 100644
--- a/actionpack/MIT-LICENSE
+++ b/actionpack/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2017 David Heinemeier Hansson
+Copyright (c) 2004-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc
index 93b2a0932a..f56230ffa0 100644
--- a/actionpack/README.rdoc
+++ b/actionpack/README.rdoc
@@ -30,7 +30,7 @@ The latest version of Action Pack can be installed with RubyGems:
$ gem install actionpack
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/actionpack
@@ -44,11 +44,11 @@ Action Pack is released under the MIT license:
== Support
-API documentation is at
+API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/actionpack/Rakefile b/actionpack/Rakefile
index 4dd7c59ce8..e99eb1723a 100644
--- a/actionpack/Rakefile
+++ b/actionpack/Rakefile
@@ -28,7 +28,7 @@ namespace :test do
end
task :lines do
- load File.expand_path("..", __dir__) + "/tools/line_statistics"
+ load File.expand_path("../tools/line_statistics", __dir__)
files = FileList["lib/**/*.rb"]
CodeTools::LineStatistics.new(files).print_loc
end
diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec
index 33d42e69d8..1dc8abf746 100644
--- a/actionpack/actionpack.gemspec
+++ b/actionpack/actionpack.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Web-flow and rendering framework putting the VC in MVC (part of Rails)."
s.description = "Web apps on Rails. Simple, battle-tested conventions for building and testing MVC web applications. Works with any Rack-compatible server."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb
index 3761054bb7..a312af6715 100644
--- a/actionpack/lib/abstract_controller/base.rb
+++ b/actionpack/lib/abstract_controller/base.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "error"
+require "abstract_controller/error"
require "active_support/configurable"
require "active_support/descendants_tracker"
require "active_support/core_ext/module/anonymous"
@@ -180,8 +180,6 @@ module AbstractController
#
# ==== Parameters
# * <tt>name</tt> - The name of an action to be tested
- #
- # :api: private
def action_method?(name)
self.class.action_methods.include?(name)
end
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb
index 146d17cf40..42bab411d2 100644
--- a/actionpack/lib/abstract_controller/callbacks.rb
+++ b/actionpack/lib/abstract_controller/callbacks.rb
@@ -103,6 +103,10 @@ module AbstractController
# :call-seq: before_action(names, block)
#
# Append a callback before actions. See _insert_callbacks for parameter details.
+ #
+ # If the callback renders or redirects, the action will not run. If there
+ # are additional callbacks scheduled to run after that callback, they are
+ # also cancelled.
##
# :method: prepend_before_action
@@ -110,6 +114,10 @@ module AbstractController
# :call-seq: prepend_before_action(names, block)
#
# Prepend a callback before actions. See _insert_callbacks for parameter details.
+ #
+ # If the callback renders or redirects, the action will not run. If there
+ # are additional callbacks scheduled to run after that callback, they are
+ # also cancelled.
##
# :method: skip_before_action
@@ -124,6 +132,10 @@ module AbstractController
# :call-seq: append_before_action(names, block)
#
# Append a callback before actions. See _insert_callbacks for parameter details.
+ #
+ # If the callback renders or redirects, the action will not run. If there
+ # are additional callbacks scheduled to run after that callback, they are
+ # also cancelled.
##
# :method: after_action
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index 41898c4c2e..8ba2b25552 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "error"
+require "abstract_controller/error"
require "action_view"
require "action_view/view_paths"
require "set"
@@ -20,7 +20,6 @@ module AbstractController
# Normalizes arguments, options and then delegates render_to_body and
# sticks the result in <tt>self.response_body</tt>.
- # :api: public
def render(*args, &block)
options = _normalize_render(*args, &block)
rendered_body = render_to_body(options)
@@ -42,19 +41,16 @@ module AbstractController
# (as ActionController extends it to be anything that
# responds to the method each), this method needs to be
# overridden in order to still return a string.
- # :api: plugin
def render_to_string(*args, &block)
options = _normalize_render(*args, &block)
render_to_body(options)
end
# Performs the actual template rendering.
- # :api: public
def render_to_body(options = {})
end
- # Returns Content-Type of rendered content
- # :api: public
+ # Returns Content-Type of rendered content.
def rendered_format
Mime[:text]
end
@@ -65,7 +61,6 @@ module AbstractController
# This method should return a hash with assigns.
# You can overwrite this configuration per controller.
- # :api: public
def view_assigns
protected_vars = _protected_ivars
variables = instance_variables
@@ -76,11 +71,11 @@ module AbstractController
}
end
+ private
# Normalize args by converting <tt>render "foo"</tt> to
# <tt>render :action => "foo"</tt> and <tt>render "foo/bar"</tt> to
# <tt>render :file => "foo/bar"</tt>.
- # :api: plugin
- def _normalize_args(action = nil, options = {})
+ def _normalize_args(action = nil, options = {}) # :doc:
if action.respond_to?(:permitted?)
if action.permitted?
action
@@ -95,20 +90,17 @@ module AbstractController
end
# Normalize options.
- # :api: plugin
- def _normalize_options(options)
+ def _normalize_options(options) # :doc:
options
end
# Process extra options.
- # :api: plugin
- def _process_options(options)
+ def _process_options(options) # :doc:
options
end
# Process the rendered format.
- # :api: private
- def _process_format(format)
+ def _process_format(format) # :nodoc:
end
def _process_variant(options)
@@ -121,8 +113,7 @@ module AbstractController
end
# Normalize args and options.
- # :api: private
- def _normalize_render(*args, &block)
+ def _normalize_render(*args, &block) # :nodoc:
options = _normalize_args(*args, &block)
_process_variant(options)
_normalize_options(options)
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index e893507baa..29d61c3ceb 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -3,8 +3,8 @@
require "active_support/rails"
require "abstract_controller"
require "action_dispatch"
-require_relative "action_controller/metal/live"
-require_relative "action_controller/metal/strong_parameters"
+require "action_controller/metal/live"
+require "action_controller/metal/strong_parameters"
module ActionController
extend ActiveSupport::Autoload
@@ -22,8 +22,10 @@ module ActionController
autoload_under "metal" do
autoload :ConditionalGet
+ autoload :ContentSecurityPolicy
autoload :Cookies
autoload :DataStreaming
+ autoload :DefaultHeaders
autoload :EtagWithTemplateDigest
autoload :EtagWithFlash
autoload :Flash
diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb
index ba9af4767e..93ffff1bd6 100644
--- a/actionpack/lib/action_controller/api.rb
+++ b/actionpack/lib/action_controller/api.rb
@@ -2,7 +2,7 @@
require "action_view"
require "action_controller"
-require_relative "log_subscriber"
+require "action_controller/log_subscriber"
module ActionController
# API Controller is a lightweight version of <tt>ActionController::Base</tt>,
@@ -122,6 +122,7 @@ module ActionController
ForceSSL,
DataStreaming,
+ DefaultHeaders,
# Before callbacks should also be executed as early as possible, so
# also include them at the bottom.
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index bbc48e6eb7..3378d6db0f 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
require "action_view"
-require_relative "log_subscriber"
-require_relative "metal/params_wrapper"
+require "action_controller/log_subscriber"
+require "action_controller/metal/params_wrapper"
module ActionController
# Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed
@@ -225,12 +225,14 @@ module ActionController
Flash,
FormBuilder,
RequestForgeryProtection,
+ ContentSecurityPolicy,
ForceSSL,
Streaming,
DataStreaming,
HttpAuthentication::Basic::ControllerMethods,
HttpAuthentication::Digest::ControllerMethods,
HttpAuthentication::Token::ControllerMethods,
+ DefaultHeaders,
# Before callbacks should also be executed as early as possible, so
# also include them at the bottom.
@@ -263,12 +265,6 @@ module ActionController
PROTECTED_IVARS
end
- def self.make_response!(request)
- ActionDispatch::Response.create.tap do |res|
- res.request = request
- end
- end
-
ActiveSupport.run_load_hooks(:action_controller_base, self)
ActiveSupport.run_load_hooks(:action_controller, self)
end
diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb
index 457884ea08..f875aa5e6b 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -230,18 +230,16 @@ module ActionController
# Returns a Rack endpoint for the given action name.
def self.action(name)
+ app = lambda { |env|
+ req = ActionDispatch::Request.new(env)
+ res = make_response! req
+ new.dispatch(name, req, res)
+ }
+
if middleware_stack.any?
- middleware_stack.build(name) do |env|
- req = ActionDispatch::Request.new(env)
- res = make_response! req
- new.dispatch(name, req, res)
- end
+ middleware_stack.build(name, app)
else
- lambda { |env|
- req = ActionDispatch::Request.new(env)
- res = make_response! req
- new.dispatch(name, req, res)
- }
+ app
end
end
diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb
new file mode 100644
index 0000000000..b8fab4ebe3
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/content_security_policy.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module ActionController #:nodoc:
+ module ContentSecurityPolicy
+ # TODO: Documentation
+ extend ActiveSupport::Concern
+
+ include AbstractController::Helpers
+ include AbstractController::Callbacks
+
+ included do
+ helper_method :content_security_policy?
+ helper_method :content_security_policy_nonce
+ end
+
+ module ClassMethods
+ def content_security_policy(enabled = true, **options, &block)
+ before_action(options) do
+ if block_given?
+ policy = current_content_security_policy
+ yield policy
+ request.content_security_policy = policy
+ end
+
+ unless enabled
+ request.content_security_policy = nil
+ end
+ end
+ end
+
+ def content_security_policy_report_only(report_only = true, **options)
+ before_action(options) do
+ request.content_security_policy_report_only = report_only
+ end
+ end
+ end
+
+ private
+
+ def content_security_policy?
+ request.content_security_policy
+ end
+
+ def content_security_policy_nonce
+ request.content_security_policy_nonce
+ end
+
+ def current_content_security_policy
+ request.content_security_policy.try(:clone) || ActionDispatch::ContentSecurityPolicy.new
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
index 882f6f3d0a..5a82ccf668 100644
--- a/actionpack/lib/action_controller/metal/data_streaming.rb
+++ b/actionpack/lib/action_controller/metal/data_streaming.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "exceptions"
+require "action_controller/metal/exceptions"
module ActionController #:nodoc:
# Methods for sending arbitrary data and for streaming files to the browser,
diff --git a/actionpack/lib/action_controller/metal/default_headers.rb b/actionpack/lib/action_controller/metal/default_headers.rb
new file mode 100644
index 0000000000..eef0602fcd
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/default_headers.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionController
+ # Allows configuring default headers that will be automatically merged into
+ # each response.
+ module DefaultHeaders
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def make_response!(request)
+ ActionDispatch::Response.create.tap do |res|
+ res.request = request
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb
index f808295720..ce9eb209fe 100644
--- a/actionpack/lib/action_controller/metal/exceptions.rb
+++ b/actionpack/lib/action_controller/metal/exceptions.rb
@@ -22,7 +22,7 @@ module ActionController
end
end
- class ActionController::UrlGenerationError < ActionControllerError #:nodoc:
+ class UrlGenerationError < ActionControllerError #:nodoc:
end
class MethodNotAllowed < ActionControllerError #:nodoc:
@@ -34,9 +34,6 @@ module ActionController
class NotImplemented < MethodNotAllowed #:nodoc:
end
- class UnknownController < ActionControllerError #:nodoc:
- end
-
class MissingFile < ActionControllerError #:nodoc:
end
@@ -53,4 +50,7 @@ module ActionController
class UnknownFormat < ActionControllerError #:nodoc:
end
+
+ class MissingExactTemplate < UnknownFormat #:nodoc:
+ end
end
diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb
index 0ba1f9f783..8d53a30e93 100644
--- a/actionpack/lib/action_controller/metal/force_ssl.rb
+++ b/actionpack/lib/action_controller/metal/force_ssl.rb
@@ -4,18 +4,10 @@ require "active_support/core_ext/hash/except"
require "active_support/core_ext/hash/slice"
module ActionController
- # This module provides a method which will redirect the browser to use the secured HTTPS
- # protocol. This will ensure that users' sensitive information will be
- # transferred safely over the internet. You _should_ always force the browser
- # to use HTTPS when you're transferring sensitive information such as
- # user authentication, account information, or credit card information.
- #
- # Note that if you are really concerned about your application security,
- # you might consider using +config.force_ssl+ in your config file instead.
- # That will ensure all the data is transferred via HTTPS, and will
- # prevent the user from getting their session hijacked when accessing the
- # site over unsecured HTTP protocol.
- module ForceSSL
+ # This module is deprecated in favor of +config.force_ssl+ in your environment
+ # config file. This will ensure all communication to non-whitelisted endpoints
+ # served by your application occurs over HTTPS.
+ module ForceSSL # :nodoc:
extend ActiveSupport::Concern
include AbstractController::Callbacks
@@ -23,45 +15,17 @@ module ActionController
URL_OPTIONS = [:protocol, :host, :domain, :subdomain, :port, :path]
REDIRECT_OPTIONS = [:status, :flash, :alert, :notice]
- module ClassMethods
- # Force the request to this particular controller or specified actions to be
- # through the HTTPS protocol.
- #
- # If you need to disable this for any reason (e.g. development) then you can use
- # an +:if+ or +:unless+ condition.
- #
- # class AccountsController < ApplicationController
- # force_ssl if: :ssl_configured?
- #
- # def ssl_configured?
- # !Rails.env.development?
- # end
- # end
- #
- # ==== URL Options
- # You can pass any of the following options to affect the redirect url
- # * <tt>host</tt> - Redirect to a different host name
- # * <tt>subdomain</tt> - Redirect to a different subdomain
- # * <tt>domain</tt> - Redirect to a different domain
- # * <tt>port</tt> - Redirect to a non-standard port
- # * <tt>path</tt> - Redirect to a different path
- #
- # ==== Redirect Options
- # You can pass any of the following options to affect the redirect status and response
- # * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently)
- # * <tt>flash</tt> - Set a flash message when redirecting
- # * <tt>alert</tt> - Set an alert message when redirecting
- # * <tt>notice</tt> - Set a notice message when redirecting
- #
- # ==== Action Options
- # You can pass any of the following options to affect the before_action callback
- # * <tt>only</tt> - The callback should be run only for this action
- # * <tt>except</tt> - The callback should be run for all actions except this action
- # * <tt>if</tt> - A symbol naming an instance method or a proc; the
- # callback will be called only when it returns a true value.
- # * <tt>unless</tt> - A symbol naming an instance method or a proc; the
- # callback will be called only when it returns a false value.
+ module ClassMethods # :nodoc:
def force_ssl(options = {})
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ Controller-level `force_ssl` is deprecated and will be removed from
+ Rails 6.1. Please enable `config.force_ssl` in your environment
+ configuration to enable the ActionDispatch::SSL middleware to more
+ fully enforce that your application communicate over HTTPS. If needed,
+ you can use `config.ssl_options` to exempt matching endpoints from
+ being redirected to HTTPS.
+ MESSAGE
+
action_options = options.slice(*ACTION_OPTIONS)
redirect_options = options.except(*ACTION_OPTIONS)
before_action(action_options) do
@@ -70,11 +34,6 @@ module ActionController
end
end
- # Redirect the existing request to use the HTTPS protocol.
- #
- # ==== Parameters
- # * <tt>host_or_options</tt> - Either a host name or any of the url and
- # redirect options available to the <tt>force_ssl</tt> method.
def force_ssl_redirect(host_or_options = nil)
unless request.ssl?
options = {
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 08d9b094f3..01676f3237 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -72,10 +72,10 @@ module ActionController
before_action(options.except(:name, :password, :realm)) do
authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password|
# This comparison uses & so that it doesn't short circuit and
- # uses `variable_size_secure_compare` so that length information
+ # uses `secure_compare` so that length information
# isn't leaked.
- ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) &
- ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password])
+ ActiveSupport::SecurityUtils.secure_compare(name, options[:name]) &
+ ActiveSupport::SecurityUtils.secure_compare(password, options[:password])
end
end
end
@@ -248,7 +248,7 @@ module ActionController
def decode_credentials(header)
ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair|
key, value = pair.split("=", 2)
- [key.strip, value.to_s.gsub(/^"|"$/, "").delete('\'')]
+ [key.strip, value.to_s.gsub(/^"|"$/, "").delete("'")]
end]
end
@@ -350,10 +350,7 @@ module ActionController
# authenticate_or_request_with_http_token do |token, options|
# # Compare the tokens in a time-constant manner, to mitigate
# # timing attacks.
- # ActiveSupport::SecurityUtils.secure_compare(
- # ::Digest::SHA256.hexdigest(token),
- # ::Digest::SHA256.hexdigest(TOKEN)
- # )
+ # ActiveSupport::SecurityUtils.secure_compare(token, 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 ac0c127cdc..d3bb58f48b 100644
--- a/actionpack/lib/action_controller/metal/implicit_render.rb
+++ b/actionpack/lib/action_controller/metal/implicit_render.rb
@@ -41,18 +41,8 @@ module ActionController
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
+ message = "#{self.class.name}\##{action_name} is missing a template for request formats: #{request.formats.map(&:to_s).join(',')}"
+ raise ActionController::MissingExactTemplate, message
else
logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
super
diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb
index 476f0843b2..be9449629f 100644
--- a/actionpack/lib/action_controller/metal/instrumentation.rb
+++ b/actionpack/lib/action_controller/metal/instrumentation.rb
@@ -83,16 +83,13 @@ module ActionController
# def cleanup_view_runtime
# super - time_taken_in_something_expensive
# end
- #
- # :api: plugin
- def cleanup_view_runtime
+ def cleanup_view_runtime # :doc:
yield
end
# Every time after an action is processed, this method is invoked
# with the payload, so you can add more information.
- # :api: plugin
- def append_info_to_payload(payload)
+ def append_info_to_payload(payload) # :doc:
payload[:view_runtime] = view_runtime
end
@@ -100,7 +97,6 @@ module ActionController
# A hook which allows other frameworks to log what happened during
# controller process action. This method should return an array
# with the messages to be added.
- # :api: plugin
def log_process_action(payload) #:nodoc:
messages, view_runtime = [], payload[:view_runtime]
messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
index f4f2381286..a678377d4f 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -112,6 +112,14 @@ module ActionController
else
self.include = m.attribute_names
end
+
+ if m.respond_to?(:nested_attributes_options) && m.nested_attributes_options.keys.any?
+ self.include += m.nested_attributes_options.keys.map do |key|
+ key.to_s.concat("_attributes")
+ end
+ end
+
+ self.include
end
end
end
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
index 5cd8568d8d..4c2b5120eb 100644
--- a/actionpack/lib/action_controller/metal/redirecting.rb
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -68,7 +68,7 @@ module ActionController
# if possible, otherwise redirects to the provided default fallback
# location.
#
- # The referrer information is pulled from the HTTP `Referer` (sic) header on
+ # The referrer information is pulled from the HTTP +Referer+ (sic) header on
# the request. This is an optional header and its presence on the request is
# subject to browser security settings and user preferences. If the request
# is missing this header, the <tt>fallback_location</tt> will be used.
@@ -79,15 +79,18 @@ module ActionController
# redirect_back fallback_location: "/images/screenshot.jpg"
# redirect_back fallback_location: posts_url
# redirect_back fallback_location: proc { edit_post_url(@post) }
+ # redirect_back fallback_location: '/', allow_other_host: false
#
- # All options that can be passed to <tt>redirect_to</tt> are accepted as
+ # ==== Options
+ # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header.
+ # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true.
+ #
+ # All other options that can be passed to <tt>redirect_to</tt> are accepted as
# options and the behavior is identical.
- def redirect_back(fallback_location:, **args)
- if referer = request.headers["Referer"]
- redirect_to referer, **args
- else
- redirect_to fallback_location, **args
- end
+ def redirect_back(fallback_location:, allow_other_host: true, **args)
+ referer = request.headers["Referer"]
+ redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
+ redirect_to redirect_to_referer ? referer : fallback_location, **args
end
def _compute_redirect_to_location(request, options) #:nodoc:
@@ -120,5 +123,11 @@ module ActionController
302
end
end
+
+ def _url_host_allowed?(url)
+ URI(url.to_s).host == request.host
+ rescue ArgumentError, URI::Error
+ false
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index 813a7e00d4..fc9cf8aaff 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "rack/session/abstract/id"
-require_relative "exceptions"
+require "action_controller/metal/exceptions"
require "active_support/security_utils"
module ActionController #:nodoc:
@@ -216,7 +216,7 @@ module ActionController #:nodoc:
# The actual before_action that is used to verify the CSRF token.
# Don't override this directly. Provide your own forgery protection
# strategy instead. If you override, you'll disable same-origin
- # `<script>` verification.
+ # <tt><script></tt> verification.
#
# Lean on the protect_from_forgery declaration to mark which actions are
# due for same-origin request verification. If protect_from_forgery is
@@ -248,8 +248,9 @@ module ActionController #:nodoc:
"If you know what you're doing, go ahead and disable forgery " \
"protection on this action to permit cross-origin JavaScript embedding."
private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
+ # :startdoc:
- # If `verify_authenticity_token` was run (indicating that we have
+ # If +verify_authenticity_token+ was run (indicating that we have
# forgery protection enabled for this request) then also verify that
# we aren't serving an unauthorized cross-origin response.
def verify_same_origin_request # :doc:
@@ -266,7 +267,7 @@ module ActionController #:nodoc:
@marked_for_same_origin_verification = request.get?
end
- # If the `verify_authenticity_token` before_action ran, verify that
+ # If the +verify_authenticity_token+ before_action ran, verify that
# JavaScript responses are only served to same-origin GET requests.
def marked_for_same_origin_verification? # :doc:
@marked_for_same_origin_verification ||= false
@@ -368,7 +369,7 @@ module ActionController #:nodoc:
end
def compare_with_real_token(token, session) # :doc:
- ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
end
def valid_per_form_csrf_token?(token, session) # :doc:
@@ -379,7 +380,7 @@ module ActionController #:nodoc:
request.request_method
)
- ActiveSupport::SecurityUtils.secure_compare(token, correct_token)
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
else
false
end
@@ -414,11 +415,21 @@ module ActionController #:nodoc:
allow_forgery_protection
end
+ NULL_ORIGIN_MESSAGE = <<~MSG
+ The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
+ means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that
+ refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the
+ best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin.
+ If you cannot change the referrer policy, you can disable origin checking with the
+ Rails.application.config.action_controller.forgery_protection_origin_check setting.
+ MSG
+
# Checks if the request originated from the same origin by looking at the
# Origin header.
def valid_request_origin? # :doc:
if forgery_protection_origin_check
# We accept blank origin headers because some user agents don't send it.
+ raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
request.origin.nil? || request.origin == request.base_url
else
true
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb
index 0b1598bf1b..8dc01a5eb9 100644
--- a/actionpack/lib/action_controller/metal/streaming.rb
+++ b/actionpack/lib/action_controller/metal/streaming.rb
@@ -183,7 +183,7 @@ module ActionController #:nodoc:
# unicorn_rails --config-file unicorn.config.rb
#
# You may also want to configure other parameters like <tt>:tcp_nodelay</tt>.
- # Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen
+ # Please check its documentation for more information: https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen
#
# If you are using Unicorn with NGINX, you may need to tweak NGINX.
# Streaming should work out of the box on Rainbows.
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index ef7c4c4c16..5a06bf86e3 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "active_support/core_ext/hash/indifferent_access"
-require "active_support/core_ext/hash/transform_values"
require "active_support/core_ext/array/wrap"
require "active_support/core_ext/string/filters"
require "active_support/core_ext/object/to_query"
@@ -335,7 +334,7 @@ module ActionController
# the same way as <tt>Hash#each_pair</tt>.
def each_pair(&block)
@parameters.each_pair do |key, value|
- yield key, convert_hashes_to_parameters(key, value)
+ yield [key, convert_hashes_to_parameters(key, value)]
end
end
alias_method :each, :each_pair
@@ -375,7 +374,7 @@ module ActionController
# Person.new(params) # => #<Person id: nil, name: "Francesco">
def permit!
each_pair do |key, value|
- Array.wrap(value).each do |v|
+ Array.wrap(value).flatten.each do |v|
v.permit! if v.respond_to? :permit!
end
end
@@ -581,19 +580,18 @@ 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
+ # 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_hashes_to_parameters(keys.first, @parameters[keys.first])
+ @parameters.dig(*keys)
end
# Returns a new <tt>ActionController::Parameters</tt> instance that
diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb
index b07f1f3d8c..6e8a95040f 100644
--- a/actionpack/lib/action_controller/metal/testing.rb
+++ b/actionpack/lib/action_controller/metal/testing.rb
@@ -12,11 +12,5 @@ module ActionController
self.params = nil
end
end
-
- module ClassMethods
- def before_filters
- _process_action_callbacks.find_all { |x| x.kind == :before }.map(&:name)
- end
- end
end
end
diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb
index 769be39004..7d42f5d931 100644
--- a/actionpack/lib/action_controller/railtie.rb
+++ b/actionpack/lib/action_controller/railtie.rb
@@ -4,7 +4,7 @@ require "rails"
require "action_controller"
require "action_dispatch/railtie"
require "abstract_controller/railties/routes_helpers"
-require_relative "railties/helpers"
+require "action_controller/railties/helpers"
require "action_view/railtie"
module ActionController
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index 74d557fc18..8f2a7e2b5f 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -7,7 +7,7 @@ require "active_support/core_ext/module/anonymous"
require "active_support/core_ext/module/redefine_method"
require "active_support/core_ext/hash/keys"
require "active_support/testing/constant_lookup"
-require_relative "template_assertions"
+require "action_controller/template_assertions"
require "rails-dom-testing"
module ActionController
@@ -256,7 +256,7 @@ module ActionController
#
# def test_create
# json = {book: { title: "Love Hina" }}.to_json
- # post :create, json
+ # post :create, body: json
# end
#
# == Special instance variables
@@ -460,10 +460,6 @@ module ActionController
def process(action, method: "GET", params: {}, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil)
check_required_ivars
- if body
- @request.set_header "RAW_POST_DATA", body
- end
-
http_method = method.to_s.upcase
@html_document = nil
@@ -478,6 +474,10 @@ module ActionController
@response.request = @request
@controller.recycle!
+ if body
+ @request.set_header "RAW_POST_DATA", body
+ end
+
@request.set_header "REQUEST_METHOD", http_method
if as
@@ -604,6 +604,7 @@ module ActionController
env.delete "action_dispatch.request.query_parameters"
env.delete "action_dispatch.request.request_parameters"
env["rack.input"] = StringIO.new
+ env.delete "RAW_POST_DATA"
env
end
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 34937f3229..0822cdc0a6 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2004-2017 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -42,6 +42,7 @@ module ActionDispatch
eager_autoload do
autoload_under "http" do
+ autoload :ContentSecurityPolicy
autoload :Request
autoload :Response
end
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index 3328ce17a0..a8febc32b3 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -133,7 +133,7 @@ module ActionDispatch
end
def generate_strong_etag(validators)
- %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
end
def cache_control_segments
diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb
new file mode 100644
index 0000000000..17e72b46ff
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb
@@ -0,0 +1,270 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/deep_dup"
+
+module ActionDispatch #:nodoc:
+ class ContentSecurityPolicy
+ class Middleware
+ CONTENT_TYPE = "Content-Type".freeze
+ POLICY = "Content-Security-Policy".freeze
+ POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ request = ActionDispatch::Request.new env
+ _, headers, _ = response = @app.call(env)
+
+ return response unless html_response?(headers)
+ return response if policy_present?(headers)
+
+ if policy = request.content_security_policy
+ nonce = request.content_security_policy_nonce
+ headers[header_name(request)] = policy.build(request.controller_instance, nonce)
+ end
+
+ response
+ end
+
+ private
+
+ def html_response?(headers)
+ if content_type = headers[CONTENT_TYPE]
+ content_type =~ /html/
+ end
+ end
+
+ def header_name(request)
+ if request.content_security_policy_report_only
+ POLICY_REPORT_ONLY
+ else
+ POLICY
+ end
+ end
+
+ def policy_present?(headers)
+ headers[POLICY] || headers[POLICY_REPORT_ONLY]
+ end
+ end
+
+ module Request
+ POLICY = "action_dispatch.content_security_policy".freeze
+ POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze
+ NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator".freeze
+ NONCE = "action_dispatch.content_security_policy_nonce".freeze
+
+ def content_security_policy
+ get_header(POLICY)
+ end
+
+ def content_security_policy=(policy)
+ set_header(POLICY, policy)
+ end
+
+ def content_security_policy_report_only
+ get_header(POLICY_REPORT_ONLY)
+ end
+
+ def content_security_policy_report_only=(value)
+ set_header(POLICY_REPORT_ONLY, value)
+ end
+
+ def content_security_policy_nonce_generator
+ get_header(NONCE_GENERATOR)
+ end
+
+ def content_security_policy_nonce_generator=(generator)
+ set_header(NONCE_GENERATOR, generator)
+ end
+
+ def content_security_policy_nonce
+ if content_security_policy_nonce_generator
+ if nonce = get_header(NONCE)
+ nonce
+ else
+ set_header(NONCE, generate_content_security_policy_nonce)
+ end
+ end
+ end
+
+ private
+
+ def generate_content_security_policy_nonce
+ content_security_policy_nonce_generator.call(self)
+ end
+ end
+
+ MAPPINGS = {
+ self: "'self'",
+ unsafe_eval: "'unsafe-eval'",
+ unsafe_inline: "'unsafe-inline'",
+ none: "'none'",
+ http: "http:",
+ https: "https:",
+ data: "data:",
+ mediastream: "mediastream:",
+ blob: "blob:",
+ filesystem: "filesystem:",
+ report_sample: "'report-sample'",
+ strict_dynamic: "'strict-dynamic'",
+ ws: "ws:",
+ wss: "wss:"
+ }.freeze
+
+ DIRECTIVES = {
+ base_uri: "base-uri",
+ child_src: "child-src",
+ connect_src: "connect-src",
+ default_src: "default-src",
+ font_src: "font-src",
+ form_action: "form-action",
+ frame_ancestors: "frame-ancestors",
+ frame_src: "frame-src",
+ img_src: "img-src",
+ manifest_src: "manifest-src",
+ media_src: "media-src",
+ object_src: "object-src",
+ script_src: "script-src",
+ style_src: "style-src",
+ worker_src: "worker-src"
+ }.freeze
+
+ NONCE_DIRECTIVES = %w[script-src].freeze
+
+ private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES
+
+ attr_reader :directives
+
+ def initialize
+ @directives = {}
+ yield self if block_given?
+ end
+
+ def initialize_copy(other)
+ @directives = other.directives.deep_dup
+ end
+
+ DIRECTIVES.each do |name, directive|
+ define_method(name) do |*sources|
+ if sources.first
+ @directives[directive] = apply_mappings(sources)
+ else
+ @directives.delete(directive)
+ end
+ end
+ end
+
+ def block_all_mixed_content(enabled = true)
+ if enabled
+ @directives["block-all-mixed-content"] = true
+ else
+ @directives.delete("block-all-mixed-content")
+ end
+ end
+
+ def plugin_types(*types)
+ if types.first
+ @directives["plugin-types"] = types
+ else
+ @directives.delete("plugin-types")
+ end
+ end
+
+ def report_uri(uri)
+ @directives["report-uri"] = [uri]
+ end
+
+ def require_sri_for(*types)
+ if types.first
+ @directives["require-sri-for"] = types
+ else
+ @directives.delete("require-sri-for")
+ end
+ end
+
+ def sandbox(*values)
+ if values.empty?
+ @directives["sandbox"] = true
+ elsif values.first
+ @directives["sandbox"] = values
+ else
+ @directives.delete("sandbox")
+ end
+ end
+
+ def upgrade_insecure_requests(enabled = true)
+ if enabled
+ @directives["upgrade-insecure-requests"] = true
+ else
+ @directives.delete("upgrade-insecure-requests")
+ end
+ end
+
+ def build(context = nil, nonce = nil)
+ build_directives(context, nonce).compact.join("; ")
+ end
+
+ private
+ def apply_mappings(sources)
+ sources.map do |source|
+ case source
+ when Symbol
+ apply_mapping(source)
+ when String, Proc
+ source
+ else
+ raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
+ end
+ end
+ end
+
+ def apply_mapping(source)
+ MAPPINGS.fetch(source) do
+ raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
+ end
+ end
+
+ def build_directives(context, nonce)
+ @directives.map do |directive, sources|
+ if sources.is_a?(Array)
+ if nonce && nonce_directive?(directive)
+ "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
+ else
+ "#{directive} #{build_directive(sources, context).join(' ')}"
+ end
+ elsif sources
+ directive
+ else
+ nil
+ end
+ end
+ end
+
+ def build_directive(sources, context)
+ sources.map { |source| resolve_source(source, context) }
+ end
+
+ def resolve_source(source, context)
+ case source
+ when String
+ source
+ when Symbol
+ source.to_s
+ when Proc
+ if context.nil?
+ raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}"
+ else
+ context.instance_exec(&source)
+ end
+ else
+ raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
+ end
+ end
+
+ def nonce_directive?(directive)
+ NONCE_DIRECTIVES.include?(directive)
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
index b7141cc1b9..ec012ad02d 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "parameter_filter"
+require "action_dispatch/http/parameter_filter"
module ActionDispatch
module Http
@@ -9,8 +9,8 @@ module ActionDispatch
# 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.
+ # sub-hashes are passed to it, where the value or the key can be replaced using
+ # String#replace or similar methods.
#
# env["action_dispatch.parameter_filter"] = [:password]
# => replaces the value to all keys matching /password/i with "[FILTERED]"
@@ -48,7 +48,7 @@ module ActionDispatch
@filtered_env ||= env_filter.filter(@env)
end
- # Reconstructed a path with all sensitive GET parameters replaced.
+ # Reconstructs a path with all sensitive GET parameters replaced.
def filtered_path
@filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
end
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index 0ca18d98a1..d7435fa8df 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -12,9 +12,6 @@ module ActionDispatch
end
# The MIME type of the HTTP request, such as Mime[:xml].
- #
- # For backward compatibility, the post \format is extracted from the
- # X-Post-Data-Format HTTP header if present.
def content_mime_type
fetch_header("action_dispatch.request.content_type") do |k|
v = if get_header("CONTENT_TYPE") =~ /^([^,\;]*)/
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index d797e90e52..295539281f 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -279,8 +279,6 @@ module Mime
def all?; false; end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
protected
attr_reader :string, :synonyms
@@ -339,4 +337,4 @@ module Mime
end
end
-require_relative "mime_types"
+require "action_dispatch/http/mime_types"
diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb
index f8e6fca36d..342e6de312 100644
--- a/actionpack/lib/action_dispatch/http/mime_types.rb
+++ b/actionpack/lib/action_dispatch/http/mime_types.rb
@@ -10,6 +10,7 @@ Mime::Type.register "text/css", :css
Mime::Type.register "text/calendar", :ics
Mime::Type.register "text/csv", :csv
Mime::Type.register "text/vcard", :vcf
+Mime::Type.register "text/vtt", :vtt, %w(vtt)
Mime::Type.register "image/png", :png, [], %w(png)
Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)
@@ -20,6 +21,18 @@ Mime::Type.register "image/svg+xml", :svg
Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe)
+Mime::Type.register "audio/mpeg", :mp3, [], %w(mp1 mp2 mp3)
+Mime::Type.register "audio/ogg", :ogg, [], %w(oga ogg spx opus)
+Mime::Type.register "audio/aac", :m4a, %w( audio/mp4 ), %w(m4a mpg4 aac)
+
+Mime::Type.register "video/webm", :webm, [], %w(webm)
+Mime::Type.register "video/mp4", :mp4, [], %w(mp4 m4v)
+
+Mime::Type.register "font/otf", :otf, [], %w(otf)
+Mime::Type.register "font/ttf", :ttf, [], %w(ttf)
+Mime::Type.register "font/woff", :woff, [], %w(woff)
+Mime::Type.register "font/woff2", :woff2, [], %w(woff2)
+
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
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index ae875eb830..8d7431fd6b 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -123,9 +123,4 @@ module ActionDispatch
end
end
end
-
- module ParamsParser
- include ActiveSupport::Deprecation::DeprecatedConstantAccessor
- deprecate_constant "ParseError", "ActionDispatch::Http::Parameters::ParseError"
- end
end
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index dee7be184a..3838b84a7a 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -3,15 +3,15 @@
require "stringio"
require "active_support/inflector"
-require_relative "headers"
+require "action_dispatch/http/headers"
require "action_controller/metal/exceptions"
require "rack/request"
-require_relative "cache"
-require_relative "mime_negotiation"
-require_relative "parameters"
-require_relative "filter_parameters"
-require_relative "upload"
-require_relative "url"
+require "action_dispatch/http/cache"
+require "action_dispatch/http/mime_negotiation"
+require "action_dispatch/http/parameters"
+require "action_dispatch/http/filter_parameters"
+require "action_dispatch/http/upload"
+require "action_dispatch/http/url"
require "active_support/core_ext/array/conversions"
module ActionDispatch
@@ -22,6 +22,7 @@ module ActionDispatch
include ActionDispatch::Http::Parameters
include ActionDispatch::Http::FilterParameters
include ActionDispatch::Http::URL
+ include ActionDispatch::ContentSecurityPolicy::Request
include Rack::Request::Env
autoload :Session, "action_dispatch/request/session"
@@ -199,6 +200,23 @@ module ActionDispatch
@headers ||= Http::Headers.new(self)
end
+ # Early Hints is an HTTP/2 status code that indicates hints to help a client start
+ # making preparations for processing the final response.
+ #
+ # If the env contains +rack.early_hints+ then the server accepts HTTP2 push for Link headers.
+ #
+ # The +send_early_hints+ method accepts a hash of links as follows:
+ #
+ # send_early_hints("Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload")
+ #
+ # If you are using +javascript_include_tag+ or +stylesheet_link_tag+ the
+ # Early Hints headers are included by default if supported.
+ def send_early_hints(links)
+ return unless env["rack.early_hints"]
+
+ env["rack.early_hints"].call(links)
+ end
+
# Returns a +String+ with the last requested path including their params.
#
# # get '/foo'
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index 0c7b153420..7e50cb6d23 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
require "active_support/core_ext/module/attribute_accessors"
-require_relative "filter_redirect"
-require_relative "cache"
+require "action_dispatch/http/filter_redirect"
+require "action_dispatch/http/cache"
require "monitor"
module ActionDispatch # :nodoc:
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index f0344fd927..35ba44005a 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -274,7 +274,7 @@ module ActionDispatch
def standard_port
case protocol
when "https://" then 443
- else 80
+ else 80
end
end
diff --git a/actionpack/lib/action_dispatch/journey.rb b/actionpack/lib/action_dispatch/journey.rb
index 903063d00f..2852efa6ae 100644
--- a/actionpack/lib/action_dispatch/journey.rb
+++ b/actionpack/lib/action_dispatch/journey.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "journey/router"
-require_relative "journey/gtg/builder"
-require_relative "journey/gtg/simulator"
-require_relative "journey/nfa/builder"
-require_relative "journey/nfa/simulator"
+require "action_dispatch/journey/router"
+require "action_dispatch/journey/gtg/builder"
+require "action_dispatch/journey/gtg/simulator"
+require "action_dispatch/journey/nfa/builder"
+require "action_dispatch/journey/nfa/simulator"
diff --git a/actionpack/lib/action_dispatch/journey/gtg/builder.rb b/actionpack/lib/action_dispatch/journey/gtg/builder.rb
index 7e3d957baa..44c31053cb 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/builder.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/builder.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "transition_table"
+require "action_dispatch/journey/gtg/transition_table"
module ActionDispatch
module Journey # :nodoc:
diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
index 6ed478f816..ea647e051a 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../nfa/dot"
+require "action_dispatch/journey/nfa/dot"
module ActionDispatch
module Journey # :nodoc:
diff --git a/actionpack/lib/action_dispatch/journey/nfa/builder.rb b/actionpack/lib/action_dispatch/journey/nfa/builder.rb
index 3135c05ffa..d22302e101 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/builder.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/builder.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "transition_table"
-require_relative "../gtg/transition_table"
+require "action_dispatch/journey/nfa/transition_table"
+require "action_dispatch/journey/gtg/transition_table"
module ActionDispatch
module Journey # :nodoc:
diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
index bdb78d8d48..56e9e3c83d 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
@@ -9,16 +9,16 @@ module ActionDispatch
" #{from} -> #{to} [label=\"#{sym || 'ε'}\"];"
}
- #memo_nodes = memos.values.flatten.map { |n|
- # label = n
- # if Journey::Route === n
- # label = "#{n.verb.source} #{n.path.spec}"
- # end
- # " #{n.object_id} [label=\"#{label}\", shape=box];"
- #}
- #memo_edges = memos.flat_map { |k, memos|
- # (memos || []).map { |v| " #{k} -> #{v.object_id};" }
- #}.uniq
+ # memo_nodes = memos.values.flatten.map { |n|
+ # label = n
+ # if Journey::Route === n
+ # label = "#{n.verb.source} #{n.path.spec}"
+ # end
+ # " #{n.object_id} [label=\"#{label}\", shape=box];"
+ # }
+ # memo_edges = memos.flat_map { |k, memos|
+ # (memos || []).map { |v| " #{k} -> #{v.object_id};" }
+ # }.uniq
<<-eodot
digraph nfa {
diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
index bfd929357b..fe55861507 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "dot"
+require "action_dispatch/journey/nfa/dot"
module ActionDispatch
module Journey # :nodoc:
diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb
index 0a84f28c1a..32f632800c 100644
--- a/actionpack/lib/action_dispatch/journey/nodes/node.rb
+++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../visitors"
+require "action_dispatch/journey/visitors"
module ActionDispatch
module Journey # :nodoc:
@@ -32,7 +32,7 @@ module ActionDispatch
end
def name
- left.tr "*:".freeze, "".freeze
+ -left.tr("*:", "")
end
def type
@@ -82,7 +82,7 @@ module ActionDispatch
def initialize(left)
super
@regexp = DEFAULT_EXP
- @name = left.tr "*:".freeze, "".freeze
+ @name = -left.tr("*:", "")
end
def default_regexp?
diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb
index 6ddfe96098..e002755bcf 100644
--- a/actionpack/lib/action_dispatch/journey/parser.rb
+++ b/actionpack/lib/action_dispatch/journey/parser.rb
@@ -8,7 +8,7 @@ require 'racc/parser.rb'
# :stopdoc:
-require_relative "parser_extras"
+require "action_dispatch/journey/parser_extras"
module ActionDispatch
module Journey
class Parser < Racc::Parser
diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y
index 850c84ea1a..f9b1a7a958 100644
--- a/actionpack/lib/action_dispatch/journey/parser.y
+++ b/actionpack/lib/action_dispatch/journey/parser.y
@@ -47,4 +47,4 @@ end
---- header
# :stopdoc:
-require_relative "parser_extras"
+require "action_dispatch/journey/parser_extras"
diff --git a/actionpack/lib/action_dispatch/journey/parser_extras.rb b/actionpack/lib/action_dispatch/journey/parser_extras.rb
index dfbc6c4529..18ec6c9b9b 100644
--- a/actionpack/lib/action_dispatch/journey/parser_extras.rb
+++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "scanner"
-require_relative "nodes/node"
+require "action_dispatch/journey/scanner"
+require "action_dispatch/journey/nodes/node"
module ActionDispatch
# :stopdoc:
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
index 2d85a89a56..537f479ee5 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -90,7 +90,7 @@ module ActionDispatch
return @separator_re unless @matchers.key?(node)
re = @matchers[node]
- "(#{re})"
+ "(#{Regexp.union(re)})"
end
def visit_GROUP(node)
@@ -183,7 +183,7 @@ module ActionDispatch
node = node.to_sym
if @requirements.key?(node)
- re = /#{@requirements[node]}|/
+ re = /#{Regexp.union(@requirements[node])}|/
@offsets.push((re.match("").length - 1) + @offsets.last)
else
@offsets << @offsets.last
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
index 9987a9bfa1..30af3ff930 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -1,16 +1,16 @@
# frozen_string_literal: true
-require_relative "router/utils"
-require_relative "routes"
-require_relative "formatter"
+require "action_dispatch/journey/router/utils"
+require "action_dispatch/journey/routes"
+require "action_dispatch/journey/formatter"
before = $-w
$-w = false
-require_relative "parser"
+require "action_dispatch/journey/parser"
$-w = before
-require_relative "route"
-require_relative "path/pattern"
+require "action_dispatch/journey/route"
+require "action_dispatch/journey/path/pattern"
module ActionDispatch
module Journey # :nodoc:
@@ -61,7 +61,7 @@ module ActionDispatch
return [status, headers, body]
end
- return [404, { "X-Cascade" => "pass" }, ["Not Found"]]
+ [404, { "X-Cascade" => "pass" }, ["Not Found"]]
end
def recognize(rails_req)
diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb
index 4ae77903fa..2a075862e9 100644
--- a/actionpack/lib/action_dispatch/journey/scanner.rb
+++ b/actionpack/lib/action_dispatch/journey/scanner.rb
@@ -34,6 +34,13 @@ module ActionDispatch
private
+ # takes advantage of String @- deduping capabilities in Ruby 2.5 upwards
+ # see: https://bugs.ruby-lang.org/issues/13077
+ def dedup_scan(regex)
+ r = @ss.scan(regex)
+ r ? -r : nil
+ end
+
def scan
case
# /
@@ -47,15 +54,15 @@ module ActionDispatch
[:OR, "|"]
when @ss.skip(/\./)
[:DOT, "."]
- when text = @ss.scan(/:\w+/)
+ when text = dedup_scan(/:\w+/)
[:SYMBOL, text]
- when text = @ss.scan(/\*\w+/)
+ when text = dedup_scan(/\*\w+/)
[:STAR, text]
when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
text.tr! "\\", ""
- [:LITERAL, text]
+ [:LITERAL, -text]
# any char
- when text = @ss.scan(/./)
+ when text = dedup_scan(/./)
[:LITERAL, text]
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index 845df500d8..c45d947904 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -49,6 +49,18 @@ module ActionDispatch
get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT
end
+ def use_authenticated_cookie_encryption
+ get_header Cookies::USE_AUTHENTICATED_COOKIE_ENCRYPTION
+ end
+
+ def encrypted_cookie_cipher
+ get_header Cookies::ENCRYPTED_COOKIE_CIPHER
+ end
+
+ def signed_cookie_digest
+ get_header Cookies::SIGNED_COOKIE_DIGEST
+ end
+
def secret_token
get_header Cookies::SECRET_TOKEN
end
@@ -64,6 +76,11 @@ module ActionDispatch
def cookies_digest
get_header Cookies::COOKIES_DIGEST
end
+
+ def cookies_rotations
+ get_header Cookies::COOKIES_ROTATIONS
+ end
+
# :startdoc:
end
@@ -144,7 +161,7 @@ module ActionDispatch
#
# * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly
# set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD.
- # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 1.
+ # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 2.
# * <tt>:expires</tt> - The time at which this cookie expires, as a \Time or ActiveSupport::Duration object.
# * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
# Default is +false+.
@@ -157,10 +174,14 @@ module ActionDispatch
ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze
+ USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption".freeze
+ ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher".freeze
+ SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest".freeze
SECRET_TOKEN = "action_dispatch.secret_token".freeze
SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
+ COOKIES_ROTATIONS = "action_dispatch.cookies_rotations".freeze
# Cookies can typically store 4096 bytes.
MAX_COOKIE_SIZE = 4096
@@ -201,12 +222,7 @@ module ActionDispatch
#
# cookies.signed[:discount] # => 45
def signed
- @signed ||=
- if upgrade_legacy_signed_cookies?
- UpgradeLegacySignedCookieJar.new(self)
- else
- SignedCookieJar.new(self)
- end
+ @signed ||= SignedKeyRotatingCookieJar.new(self)
end
# Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
@@ -223,18 +239,11 @@ module ActionDispatch
# Example:
#
# cookies.encrypted[:discount] = 45
- # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
+ # # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/
#
# cookies.encrypted[:discount] # => 45
def encrypted
- @encrypted ||=
- if upgrade_legacy_signed_cookies?
- UpgradeLegacyEncryptedCookieJar.new(self)
- elsif upgrade_legacy_hmac_aes_cbc_cookies?
- UpgradeLegacyHmacAesCbcCookieJar.new(self)
- else
- EncryptedCookieJar.new(self)
- end
+ @encrypted ||= EncryptedKeyRotatingCookieJar.new(self)
end
# Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
@@ -255,34 +264,18 @@ module ActionDispatch
end
def upgrade_legacy_hmac_aes_cbc_cookies?
- request.secret_key_base.present? &&
- request.authenticated_encrypted_cookie_salt.present? &&
- request.encrypted_signed_cookie_salt.present? &&
- request.encrypted_cookie_salt.present?
+ request.secret_key_base.present? &&
+ request.encrypted_signed_cookie_salt.present? &&
+ request.encrypted_cookie_salt.present? &&
+ request.use_authenticated_cookie_encryption
end
- end
- # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
- # to the Message{Encryptor,Verifier} allows us to handle the
- # (de)serialization step within the cookie jar, which gives us the
- # opportunity to detect and migrate legacy cookies.
- module VerifyAndUpgradeLegacySignedMessage # :nodoc:
- def initialize(*args)
- super
- @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
- end
-
- def verify_and_upgrade_legacy_signed_message(name, signed_message)
- deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value|
- self[name] = { value: value }
+ def encrypted_cookie_cipher
+ request.encrypted_cookie_cipher || "aes-256-gcm"
end
- rescue ActiveSupport::MessageVerifier::InvalidSignature
- nil
- end
- private
- def parse(name, signed_message)
- super || verify_and_upgrade_legacy_signed_message(name, signed_message)
+ def signed_cookie_digest
+ request.signed_cookie_digest || "SHA1"
end
end
@@ -345,6 +338,9 @@ module ActionDispatch
end
alias :has_key? :key?
+ # Returns the cookies as Hash.
+ alias :to_hash :to_h
+
def update(other_hash)
@cookies.update other_hash.stringify_keys
self
@@ -524,6 +520,7 @@ module ActionDispatch
module SerializedCookieJars # :nodoc:
MARSHAL_SIGNATURE = "\x04\x08".freeze
+ SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
protected
def needs_migration?(value)
@@ -534,12 +531,16 @@ module ActionDispatch
serializer.dump(value)
end
- def deserialize(name, value)
+ def deserialize(name)
+ rotate = false
+ value = yield -> { rotate = true }
+
if value
- if needs_migration?(value)
- Marshal.load(value).tap do |v|
- self[name] = { value: v }
- end
+ case
+ when needs_migration?(value)
+ self[name] = Marshal.load(value)
+ when rotate
+ self[name] = serializer.load(value)
else
serializer.load(value)
end
@@ -561,24 +562,31 @@ module ActionDispatch
def digest
request.cookies_digest || "SHA1"
end
-
- def key_generator
- request.key_generator
- end
end
- class SignedCookieJar < AbstractCookieJar # :nodoc:
+ class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
include SerializedCookieJars
def initialize(parent_jar)
super
- secret = key_generator.generate_key(request.signed_cookie_salt)
- @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
+
+ secret = request.key_generator.generate_key(request.signed_cookie_salt)
+ @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
+
+ request.cookies_rotations.signed.each do |*secrets, **options|
+ @verifier.rotate(*secrets, serializer: SERIALIZER, **options)
+ end
+
+ if upgrade_legacy_signed_cookies?
+ @verifier.rotate request.secret_token, serializer: SERIALIZER
+ end
end
private
def parse(name, signed_message)
- deserialize name, @verifier.verified(signed_message)
+ deserialize(name) do |rotate|
+ @verifier.verified(signed_message, on_rotation: rotate)
+ end
end
def commit(options)
@@ -588,37 +596,47 @@ module ActionDispatch
end
end
- # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
- # secrets.secret_token and secret_key_base are both set. It reads
- # legacy cookies signed with the old dummy key generator and signs and
- # re-saves them using the new key generator to provide a smooth upgrade path.
- class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
- include VerifyAndUpgradeLegacySignedMessage
- end
-
- class EncryptedCookieJar < AbstractCookieJar # :nodoc:
+ class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
include SerializedCookieJars
def initialize(parent_jar)
super
- if ActiveSupport::LegacyKeyGenerator === key_generator
- raise "You didn't set secret_key_base, which is required for this cookie jar. " \
- "Read the upgrade documentation to learn more about this new config option."
+ if request.use_authenticated_cookie_encryption
+ key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
+ secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len)
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER)
+ else
+ key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len)
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER)
+ end
+
+ request.cookies_rotations.encrypted.each do |*secrets, **options|
+ @encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
end
- cipher = "aes-256-gcm"
- key_len = ActiveSupport::MessageEncryptor.key_len(cipher)
- secret = key_generator.generate_key(request.authenticated_encrypted_cookie_salt || "")[0, key_len]
+ if upgrade_legacy_hmac_aes_cbc_cookies?
+ legacy_cipher = "aes-256-cbc"
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher))
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
+
+ @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
+ end
- @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
+ if upgrade_legacy_signed_cookies?
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER)
+ end
end
private
def parse(name, encrypted_message)
- deserialize name, @encryptor.decrypt_and_verify(encrypted_message)
- rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
- nil
+ deserialize(name) do |rotate|
+ @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate)
+ end
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
+ parse_legacy_signed_message(name, encrypted_message)
end
def commit(options)
@@ -626,39 +644,15 @@ module ActionDispatch
raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
end
- end
- # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
- # instead of EncryptedCookieJar if secrets.secret_token and secret_key_base
- # are both set. It reads legacy cookies signed with the old dummy key generator and
- # encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
- class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
- include VerifyAndUpgradeLegacySignedMessage
- end
+ def parse_legacy_signed_message(name, legacy_signed_message)
+ if defined?(@legacy_verifier)
+ deserialize(name) do |rotate|
+ rotate.call
- # UpgradeLegacyHmacAesCbcCookieJar is used by ActionDispatch::Session::CookieStore
- # to upgrade cookies encrypted with AES-256-CBC with HMAC to AES-256-GCM
- class UpgradeLegacyHmacAesCbcCookieJar < EncryptedCookieJar
- def initialize(parent_jar)
- super
-
- secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len]
- sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "")
-
- @legacy_encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
- end
-
- def decrypt_and_verify_legacy_encrypted_message(name, signed_message)
- deserialize(name, @legacy_encryptor.decrypt_and_verify(signed_message)).tap do |value|
- self[name] = { value: value }
- end
- rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
- nil
- end
-
- private
- def parse(name, signed_message)
- super || decrypt_and_verify_legacy_encrypted_message(name, signed_message)
+ @legacy_verifier.verified(legacy_signed_message)
+ end
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index 3006cd97ce..33edad8bd9 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "../http/request"
-require_relative "exception_wrapper"
-require_relative "../routing/inspector"
+require "action_dispatch/http/request"
+require "action_dispatch/middleware/exception_wrapper"
+require "action_dispatch/routing/inspector"
require "action_view"
require "action_view/base"
@@ -50,10 +50,18 @@ module ActionDispatch
end
end
- def initialize(app, routes_app = nil, response_format = :default)
+ cattr_reader :interceptors, instance_accessor: false, default: []
+
+ def self.register_interceptor(object = nil, &block)
+ interceptor = object || block
+ interceptors << interceptor
+ end
+
+ def initialize(app, routes_app = nil, response_format = :default, interceptors = self.class.interceptors)
@app = app
@routes_app = routes_app
@response_format = response_format
+ @interceptors = interceptors
end
def call(env)
@@ -67,12 +75,26 @@ module ActionDispatch
response
rescue Exception => exception
+ invoke_interceptors(request, exception)
raise exception unless request.show_exceptions?
render_exception(request, exception)
end
private
+ def invoke_interceptors(request, exception)
+ backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
+
+ @interceptors.each do |interceptor|
+ begin
+ interceptor.call(request, exception)
+ rescue Exception
+ log_error(request, wrapper)
+ end
+ end
+ end
+
def render_exception(request, exception)
backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index 4f69abfa6f..f05c69137b 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -12,6 +12,7 @@ module ActionDispatch
"ActionController::UnknownHttpMethod" => :method_not_allowed,
"ActionController::NotImplemented" => :not_implemented,
"ActionController::UnknownFormat" => :not_acceptable,
+ "ActionController::MissingExactTemplate" => :not_acceptable,
"ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
"ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
"ActionDispatch::Http::Parameters::ParseError" => :bad_request,
@@ -22,10 +23,12 @@ module ActionDispatch
)
cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!(
- "ActionView::MissingTemplate" => "missing_template",
- "ActionController::RoutingError" => "routing_error",
- "AbstractController::ActionNotFound" => "unknown_action",
- "ActionView::Template::Error" => "template_error"
+ "ActionView::MissingTemplate" => "missing_template",
+ "ActionController::RoutingError" => "routing_error",
+ "AbstractController::ActionNotFound" => "unknown_action",
+ "ActiveRecord::StatementInvalid" => "invalid_statement",
+ "ActionView::Template::Error" => "template_error",
+ "ActionController::MissingExactTemplate" => "missing_exact_template",
)
attr_reader :backtrace_cleaner, :exception, :line_number, :file
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index 3e11846778..fd05eec172 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -73,7 +73,7 @@ module ActionDispatch
end
end
- def reset_session # :nodoc
+ def reset_session # :nodoc:
super
self.flash = nil
end
diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb
index 805d3f2148..da2871b551 100644
--- a/actionpack/lib/action_dispatch/middleware/request_id.rb
+++ b/actionpack/lib/action_dispatch/middleware/request_id.rb
@@ -30,7 +30,7 @@ module ActionDispatch
private
def make_request_id(request_id)
if request_id.presence
- request_id.gsub(/[^\w\-]/, "".freeze).first(255)
+ request_id.gsub(/[^\w\-@]/, "".freeze).first(255)
else
internal_request_id
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
index e054fefc9b..5b0be96223 100644
--- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -3,8 +3,8 @@
require "rack/utils"
require "rack/request"
require "rack/session/abstract/id"
-require_relative "../cookies"
-require_relative "../../request/session"
+require "action_dispatch/middleware/cookies"
+require "action_dispatch/request/session"
module ActionDispatch
module Session
diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
index c84bc8bfad..a6d965a644 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "abstract_store"
+require "action_dispatch/middleware/session/abstract_store"
module ActionDispatch
module Session
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index b0514a96d8..4ea96196d3 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "active_support/core_ext/hash/keys"
-require_relative "abstract_store"
+require "action_dispatch/middleware/session/abstract_store"
require "rack/session/cookie"
module ActionDispatch
diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
index f0aec39c9c..914df3a2b1 100644
--- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "abstract_store"
+require "action_dispatch/middleware/session/abstract_store"
begin
require "rack/session/dalli"
rescue LoadError => e
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
index d2e739d27f..3c88afd4d3 100644
--- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../http/request"
-require_relative "exception_wrapper"
+require "action_dispatch/http/request"
+require "action_dispatch/middleware/exception_wrapper"
module ActionDispatch
# This middleware rescues any exception returned by the application
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index 45290b6ac3..240269d1c7 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -1,50 +1,56 @@
# frozen_string_literal: true
module ActionDispatch
- # This middleware is added to the stack when `config.force_ssl = true`, and is passed
- # the options set in `config.ssl_options`. It does three jobs to enforce secure HTTP
+ # This middleware is added to the stack when <tt>config.force_ssl = true</tt>, and is passed
+ # the options set in +config.ssl_options+. It does three jobs to enforce secure HTTP
# requests:
#
- # 1. TLS redirect: Permanently redirects http:// requests to https://
- # with the same URL host, path, etc. Enabled by default. Set `config.ssl_options`
- # to modify the destination URL
- # (e.g. `redirect: { host: "secure.widgets.com", port: 8080 }`), or set
- # `redirect: false` to disable this feature.
+ # 1. <b>TLS redirect</b>: Permanently redirects +http://+ requests to +https://+
+ # with the same URL host, path, etc. Enabled by default. Set +config.ssl_options+
+ # to modify the destination URL
+ # (e.g. <tt>redirect: { host: "secure.widgets.com", port: 8080 }</tt>), or set
+ # <tt>redirect: false</tt> to disable this feature.
#
- # 2. Secure cookies: Sets the `secure` flag on cookies to tell browsers they
- # mustn't be sent along with http:// requests. Enabled by default. Set
- # `config.ssl_options` with `secure_cookies: false` to disable this feature.
+ # Requests can opt-out of redirection with +exclude+:
#
- # 3. HTTP Strict Transport Security (HSTS): Tells the browser to remember
- # this site as TLS-only and automatically redirect non-TLS requests.
- # Enabled by default. Configure `config.ssl_options` with `hsts: false` to disable.
+ # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
#
- # Set `config.ssl_options` with `hsts: { … }` to configure HSTS:
- # * `expires`: How long, in seconds, these settings will stick. The minimum
- # required to qualify for browser preload lists is `18.weeks`. Defaults to
- # `180.days` (recommended).
- # * `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 `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
- # gap, browser vendors include a baked-in list of HSTS-enabled sites.
- # Go to https://hstspreload.appspot.com to submit your site for inclusion.
- # Defaults to `false`.
+ # Cookies will not be flagged as secure for excluded requests.
#
- # To turn off HSTS, omitting the header is not enough. Browsers will remember the
- # 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 }`.
+ # 2. <b>Secure cookies</b>: Sets the +secure+ flag on cookies to tell browsers they
+ # must not be sent along with +http://+ requests. Enabled by default. Set
+ # +config.ssl_options+ with <tt>secure_cookies: false</tt> to disable this feature.
#
- # Requests can opt-out of redirection with `exclude`:
+ # 3. <b>HTTP Strict Transport Security (HSTS)</b>: Tells the browser to remember
+ # this site as TLS-only and automatically redirect non-TLS requests.
+ # Enabled by default. Configure +config.ssl_options+ with <tt>hsts: false</tt> to disable.
#
- # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
+ # Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS:
+ #
+ # * +expires+: How long, in seconds, these settings will stick. The minimum
+ # required to qualify for browser preload lists is 1 year. Defaults to
+ # 1 year (recommended).
+ #
+ # * +subdomains+: Set to +true+ to tell the browser to apply these settings
+ # to all subdomains. This protects your cookies from interception by a
+ # 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 <i>except the
+ # first visit</i> since it hasn't seen your HSTS header yet. To close this
+ # gap, browser vendors include a baked-in list of HSTS-enabled sites.
+ # Go to https://hstspreload.org to submit your site for inclusion.
+ # Defaults to +false+.
+ #
+ # To turn off HSTS, omitting the header is not enough. Browsers will remember the
+ # original HSTS directive until it expires. Instead, use the header to tell browsers to
+ # expire HSTS immediately. Setting <tt>hsts: false</tt> is a shortcut for
+ # <tt>hsts: { expires: 0 }</tt>.
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.
- HSTS_EXPIRES_IN = 15552000
+ # :stopdoc:
+
+ # Default to 1 year, the minimum for browser preload lists.
+ HSTS_EXPIRES_IN = 31536000
def self.default_hsts_options
{ expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
@@ -67,7 +73,7 @@ module ActionDispatch
if request.ssl?
@app.call(env).tap do |status, headers, body|
set_hsts_header! headers
- flag_cookies_as_secure! headers if @secure_cookies
+ flag_cookies_as_secure! headers if @secure_cookies && !@exclude.call(request)
end
else
return redirect_to_https request unless @exclude.call(request)
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index 23492e14eb..8130bfe2e7 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -16,7 +16,7 @@ module ActionDispatch
# does not exist, a 404 "File not Found" response will be returned.
class FileHandler
def initialize(root, index: "index", headers: {})
- @root = root.chomp("/")
+ @root = root.chomp("/").b
@file_server = ::Rack::File.new(@root, headers)
@index = index
end
@@ -35,7 +35,7 @@ module ActionDispatch
paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
if match = paths.detect { |p|
- path = File.join(@root, p.dup.force_encoding(Encoding::UTF_8))
+ path = File.join(@root, p.b)
begin
File.file?(path) && File.readable?(path)
rescue SystemCallError
@@ -43,7 +43,7 @@ module ActionDispatch
end
}
- return ::Rack::Utils.escape_path(match)
+ return ::Rack::Utils.escape_path(match).b
end
end
@@ -69,7 +69,7 @@ module ActionDispatch
headers["Vary"] = "Accept-Encoding" if gzip_path
- return [status, headers, body]
+ [status, headers, body]
ensure
request.path_info = path
end
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb
new file mode 100644
index 0000000000..e1b129ccc5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb
@@ -0,0 +1,21 @@
+<header>
+ <h1>
+ <%= @exception.class.to_s %>
+ <% if @request.parameters['controller'] %>
+ in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
+ <% end %>
+ </h1>
+</header>
+
+<div id="container">
+ <h2>
+ <%= h @exception.message %>
+ <% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %>
+ <br />To resolve this issue run: bin/rails active_storage:install
+ <% end %>
+ </h2>
+
+ <%= render template: "rescues/_source" %>
+ <%= render template: "rescues/_trace" %>
+ <%= render template: "rescues/_request_and_response" %>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb
new file mode 100644
index 0000000000..033518cf8a
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb
@@ -0,0 +1,13 @@
+<%= @exception.class.to_s %><%
+ if @request.parameters['controller']
+%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
+<% end %>
+
+<%= @exception.message %>
+<% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %>
+To resolve this issue run: bin/rails active_storage:install
+<% end %>
+
+<%= render template: "rescues/_source" %>
+<%= render template: "rescues/_trace" %>
+<%= render template: "rescues/_request_and_response" %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
index e0509f56f4..39ea25bdfc 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
@@ -106,6 +106,7 @@
.line {
padding-left: 10px;
+ white-space: pre;
}
.line:hover {
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb
new file mode 100644
index 0000000000..76ab1691b5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb
@@ -0,0 +1,19 @@
+<header>
+ <h1>No template for interactive request</h1>
+</header>
+
+<div id="container">
+ <h2><%= h @exception.message %></h2>
+
+ <p class="summary">
+ <strong>NOTE!</strong><br>
+ Unless told otherwise, Rails expects an action to render a template with the same name,<br>
+ contained in a folder named after its controller.
+
+ If this controller is an API responding with 204 (No Content), <br>
+ which does not require a template,
+ then this error will occur when trying to access it via browser,<br>
+ since we expect an HTML template
+ to be rendered for such requests. If that's the case, carry on.
+ </p>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb
new file mode 100644
index 0000000000..fcdbe6069d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb
@@ -0,0 +1,3 @@
+Missing exact template
+
+<%= @exception.message %>
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index 4743a7ce61..eb6fbca6ba 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "action_dispatch"
+require "active_support/messages/rotation_configuration"
module ActionDispatch
class Railtie < Rails::Railtie # :nodoc:
@@ -18,15 +19,21 @@ module ActionDispatch
config.action_dispatch.signed_cookie_salt = "signed cookie"
config.action_dispatch.encrypted_cookie_salt = "encrypted cookie"
config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie"
+ config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie"
config.action_dispatch.use_authenticated_cookie_encryption = false
config.action_dispatch.perform_deep_munge = true
config.action_dispatch.default_headers = {
"X-Frame-Options" => "SAMEORIGIN",
"X-XSS-Protection" => "1; mode=block",
- "X-Content-Type-Options" => "nosniff"
+ "X-Content-Type-Options" => "nosniff",
+ "X-Download-Options" => "noopen",
+ "X-Permitted-Cross-Domain-Policies" => "none",
+ "Referrer-Policy" => "strict-origin-when-cross-origin"
}
+ config.action_dispatch.cookies_rotations = ActiveSupport::Messages::RotationConfiguration.new
+
config.eager_load_namespaces << ActionDispatch
initializer "action_dispatch.configure" do |app|
@@ -39,8 +46,6 @@ module ActionDispatch
ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses)
ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates)
- config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie" if config.action_dispatch.use_authenticated_cookie_encryption
-
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
diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb
index d86d0b10c2..bc5e0670e0 100644
--- a/actionpack/lib/action_dispatch/request/session.rb
+++ b/actionpack/lib/action_dispatch/request/session.rb
@@ -93,6 +93,14 @@ module ActionDispatch
@delegate[key.to_s]
end
+ # Returns the nested value specified by the sequence of keys, returning
+ # +nil+ if any intermediate step is +nil+.
+ def dig(*keys)
+ load_for_read!
+ keys = keys.map.with_index { |key, i| i.zero? ? key.to_s : key }
+ @delegate.dig(*keys)
+ end
+
# Returns true if the session has the given key or false.
def has_key?(key)
load_for_read!
@@ -130,6 +138,7 @@ module ActionDispatch
load_for_read!
@delegate.dup.delete_if { |_, v| v.nil? }
end
+ alias :to_h :to_hash
# Updates the session with given Hash.
#
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index 72f7407c6e..5cde677051 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -243,8 +243,9 @@ module ActionDispatch
#
# rails routes
#
- # Target specific controllers by prefixing the command with <tt>-c</tt> option.
- #
+ # Target a specific controller with <tt>-c</tt>, or grep routes
+ # using <tt>-g</tt>. Useful in conjunction with <tt>--expanded</tt>
+ # which displays routes vertically.
module Routing
extend ActiveSupport::Autoload
diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb
index e911b6537b..28bb20d688 100644
--- a/actionpack/lib/action_dispatch/routing/endpoint.rb
+++ b/actionpack/lib/action_dispatch/routing/endpoint.rb
@@ -5,8 +5,13 @@ module ActionDispatch
class Endpoint # :nodoc:
def dispatcher?; false; end
def redirect?; false; end
- def matches?(req); true; end
- def app; self; end
+ def matches?(req); true; end
+ def app; self; end
+ def rack_app; app; end
+
+ def engine?
+ rack_app.is_a?(Class) && rack_app < Rails::Engine
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index b2868b7427..bae50f6a43 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "delegate"
-require "active_support/core_ext/string/strip"
+require "io/console/size"
module ActionDispatch
module Routing
@@ -15,7 +15,7 @@ module ActionDispatch
end
def rack_app
- app.app
+ app.rack_app
end
def path
@@ -47,7 +47,7 @@ module ActionDispatch
end
def engine?
- rack_app.respond_to?(:routes)
+ app.engine?
end
end
@@ -61,11 +61,11 @@ module ActionDispatch
@routes = routes
end
- def format(formatter, filter = nil)
+ def format(formatter, filter = {})
routes_to_display = filter_routes(normalize_filter(filter))
routes = collect_routes(routes_to_display)
if routes.none?
- formatter.no_routes(collect_routes(@routes))
+ formatter.no_routes(collect_routes(@routes), filter)
return formatter.result
end
@@ -81,12 +81,12 @@ module ActionDispatch
end
private
-
def normalize_filter(filter)
- if filter.is_a?(Hash) && filter[:controller]
+ if filter[:controller]
{ controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ }
- elsif filter
- { controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ }
+ elsif filter[:grep]
+ { controller: /#{filter[:grep]}/, action: /#{filter[:grep]}/,
+ verb: /#{filter[:grep]}/, name: /#{filter[:grep]}/, path: /#{filter[:grep]}/ }
end
end
@@ -126,62 +126,111 @@ module ActionDispatch
end
end
- class ConsoleFormatter
- def initialize
- @buffer = []
- end
+ module ConsoleFormatter
+ class Base
+ def initialize
+ @buffer = []
+ end
- def result
- @buffer.join("\n")
- end
+ def result
+ @buffer.join("\n")
+ end
- def section_title(title)
- @buffer << "\n#{title}:"
- end
+ def section_title(title)
+ end
- def section(routes)
- @buffer << draw_section(routes)
- end
+ def section(routes)
+ end
- def header(routes)
- @buffer << draw_header(routes)
- end
+ def header(routes)
+ end
- def no_routes(routes)
- @buffer <<
- if routes.none?
- <<-MESSAGE.strip_heredoc
- You don't have any routes defined!
+ def no_routes(routes, filter)
+ @buffer <<
+ if routes.none?
+ <<~MESSAGE
+ You don't have any routes defined!
+
+ Please add some routes in config/routes.rb.
+ MESSAGE
+ elsif filter.key?(:controller)
+ "No routes were found for this controller."
+ elsif filter.key?(:grep)
+ "No routes were found for this grep pattern."
+ end
- Please add some routes in config/routes.rb.
- MESSAGE
- else
- "No routes were found for this controller"
+ @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html."
end
- @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html."
end
- private
- def draw_section(routes)
- header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length)
- name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
+ class Sheet < Base
+ def section_title(title)
+ @buffer << "\n#{title}:"
+ end
- routes.map do |r|
- "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
- end
+ def section(routes)
+ @buffer << draw_section(routes)
+ end
+
+ def header(routes)
+ @buffer << draw_header(routes)
end
- def draw_header(routes)
- name_width, verb_width, path_width = widths(routes)
+ private
+
+ def draw_section(routes)
+ header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length)
+ name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
+
+ routes.map do |r|
+ "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ end
+ end
+
+ def draw_header(routes)
+ name_width, verb_width, path_width = widths(routes)
+
+ "#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action"
+ end
+
+ def widths(routes)
+ [routes.map { |r| r[:name].length }.max || 0,
+ routes.map { |r| r[:verb].length }.max || 0,
+ routes.map { |r| r[:path].length }.max || 0]
+ end
+ end
- "#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action"
+ class Expanded < Base
+ def section_title(title)
+ @buffer << "\n#{"[ #{title} ]"}"
end
- def widths(routes)
- [routes.map { |r| r[:name].length }.max || 0,
- routes.map { |r| r[:verb].length }.max || 0,
- routes.map { |r| r[:path].length }.max || 0]
+ def section(routes)
+ @buffer << draw_expanded_section(routes)
end
+
+ private
+
+ def draw_expanded_section(routes)
+ routes.map.each_with_index do |r, i|
+ <<~MESSAGE.chomp
+ #{route_header(index: i + 1)}
+ Prefix | #{r[:name]}
+ Verb | #{r[:verb]}
+ URI | #{r[:path]}
+ Controller#Action | #{r[:reqs]}
+ MESSAGE
+ end
+ end
+
+ def route_header(index:)
+ console_width = IO.console_size.second
+ header_prefix = "--[ Route #{index} ]"
+ dash_remainder = [console_width - header_prefix.size, 0].max
+
+ "#{header_prefix}#{'-' * dash_remainder}"
+ end
+ end
end
class HtmlTableFormatter
@@ -203,7 +252,7 @@ module ActionDispatch
end
def no_routes(*)
- @buffer << <<-MESSAGE.strip_heredoc
+ @buffer << <<~MESSAGE
<p>You don't have any routes defined!</p>
<ul>
<li>Please add some routes in <tt>config/routes.rb</tt>.</li>
@@ -212,7 +261,7 @@ module ActionDispatch
<a href="http://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>.
</li>
</ul>
- MESSAGE
+ MESSAGE
end
def result
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 2b43ade081..d9dd24935b 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -4,8 +4,8 @@ require "active_support/core_ext/hash/slice"
require "active_support/core_ext/enumerable"
require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/regexp"
-require_relative "redirection"
-require_relative "endpoint"
+require "action_dispatch/routing/redirection"
+require "action_dispatch/routing/endpoint"
module ActionDispatch
module Routing
@@ -475,6 +475,16 @@ module ActionDispatch
#
# resources :users, param: :name
#
+ # The +users+ resource here will have the following routes generated for it:
+ #
+ # GET /users(.:format)
+ # POST /users(.:format)
+ # GET /users/new(.:format)
+ # GET /users/:name/edit(.:format)
+ # GET /users/:name(.:format)
+ # PATCH/PUT /users/:name(.:format)
+ # DELETE /users/:name(.:format)
+ #
# You can override <tt>ActiveRecord::Base#to_param</tt> of a related
# model to construct a URL:
#
@@ -484,8 +494,8 @@ module ActionDispatch
# end
# end
#
- # user = User.find_by(name: 'Phusion')
- # user_path(user) # => "/users/Phusion"
+ # user = User.find_by(name: 'Phusion')
+ # user_path(user) # => "/users/Phusion"
#
# [:path]
# The path prefix for the routes.
@@ -601,7 +611,7 @@ module ActionDispatch
end
raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
- raise ArgumentError, <<-MSG.strip_heredoc unless path
+ raise ArgumentError, <<~MSG unless path
Must be called with mount point
mount SomeRackApp, at: "some_route"
@@ -654,6 +664,7 @@ module ActionDispatch
def define_generate_prefix(app, name)
_route = @set.named_routes.get name
_routes = @set
+ _url_helpers = @set.url_helpers
script_namer = ->(options) do
prefix_options = options.slice(*_route.segment_keys)
@@ -665,7 +676,7 @@ module ActionDispatch
# We must actually delete prefix segment keys to avoid passing them to next url_for.
_route.segment_keys.each { |k| options.delete(k) }
- _routes.url_helpers.send("#{name}_path", prefix_options)
+ _url_helpers.send("#{name}_path", prefix_options)
end
app.routes.define_mounted_helper(name, script_namer)
@@ -1265,7 +1276,7 @@ module ActionDispatch
# POST /profile
#
# === Options
- # Takes same options as +resources+.
+ # Takes same options as resources[rdoc-ref:#resources]
def resource(*resources, &block)
options = resources.extract_options!.dup
@@ -1330,7 +1341,7 @@ module ActionDispatch
# DELETE /photos/:photo_id/comments/:id
#
# === Options
- # Takes same options as <tt>Base#match</tt> as well as:
+ # Takes same options as match[rdoc-ref:Base#match] as well as:
#
# [:path_names]
# Allows you to change the segment component of the +edit+ and +new+ actions.
@@ -1563,7 +1574,7 @@ module ActionDispatch
# Matches a URL pattern to one or more routes.
# For more information, see match[rdoc-ref:Base#match].
#
- # match 'path' => 'controller#action', via: patch
+ # match 'path' => 'controller#action', via: :patch
# match 'path', to: 'controller#action', via: :post
# match 'path', 'otherpath', on: :member, via: :get
def match(path, *rest, &block)
@@ -2036,7 +2047,7 @@ module ActionDispatch
end
module CustomUrls
- # Define custom url helpers that will be added to the application's
+ # Define custom URL helpers that will be added to the application's
# routes. This allows you to override and/or replace the default behavior
# of routing helpers, e.g:
#
@@ -2056,11 +2067,11 @@ module ActionDispatch
# arguments for +url_for+ which will actually build the URL string. This can
# be one of the following:
#
- # * A string, which is treated as a generated URL
- # * A hash, e.g. { controller: "pages", action: "index" }
- # * An array, which is passed to `polymorphic_url`
- # * An Active Model instance
- # * An Active Model class
+ # * A string, which is treated as a generated URL
+ # * A hash, e.g. <tt>{ controller: "pages", action: "index" }</tt>
+ # * An array, which is passed to +polymorphic_url+
+ # * An Active Model instance
+ # * An Active Model class
#
# NOTE: Other URL helpers can be called in the block but be careful not to invoke
# your custom URL helper again otherwise it will result in a stack overflow error.
@@ -2072,9 +2083,9 @@ module ActionDispatch
# [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ]
# end
#
- # In this instance the +params+ object comes from the context in which the the
+ # In this instance the +params+ object comes from the context in which the
# block is executed, e.g. generating a URL inside a controller action or a view.
- # If the block is executed where there isn't a params object such as this:
+ # If the block is executed where there isn't a +params+ object such as this:
#
# Rails.application.routes.url_helpers.browse_path
#
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index 6da869c0c2..e17ccaf986 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -120,8 +120,7 @@ module ActionDispatch
opts
end
- # Returns the path component of a URL for the given record. It uses
- # <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>.
+ # Returns the path component of a URL for the given record.
def polymorphic_path(record_or_hash_or_array, options = {})
if Hash === record_or_hash_or_array
options = record_or_hash_or_array.merge(options)
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
index 2e2bc87b57..143a4b3d62 100644
--- a/actionpack/lib/action_dispatch/routing/redirection.rb
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
-require_relative "../http/request"
+require "action_dispatch/http/request"
require "active_support/core_ext/uri"
require "active_support/core_ext/array/extract_options"
require "rack/utils"
require "action_controller/metal/exceptions"
-require_relative "endpoint"
+require "action_dispatch/routing/endpoint"
module ActionDispatch
module Routing
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 445e86b13c..1134279a7f 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -1,14 +1,13 @@
# frozen_string_literal: true
-require_relative "../journey"
+require "action_dispatch/journey"
require "active_support/core_ext/object/to_query"
-require "active_support/core_ext/hash/slice"
require "active_support/core_ext/module/redefine_method"
require "active_support/core_ext/module/remove_method"
require "active_support/core_ext/array/extract_options"
require "action_controller/metal/exceptions"
-require_relative "../http/request"
-require_relative "endpoint"
+require "action_dispatch/http/request"
+require "action_dispatch/routing/endpoint"
module ActionDispatch
module Routing
@@ -36,7 +35,7 @@ module ActionDispatch
if @raise_on_name_error
raise
else
- return [404, { "X-Cascade" => "pass" }, []]
+ [404, { "X-Cascade" => "pass" }, []]
end
end
@@ -154,13 +153,13 @@ module ActionDispatch
url_name = :"#{name}_url"
@path_helpers_module.module_eval do
- define_method(path_name) do |*args|
+ redefine_method(path_name) do |*args|
helper.call(self, args, true)
end
end
@url_helpers_module.module_eval do
- define_method(url_name) do |*args|
+ redefine_method(url_name) do |*args|
helper.call(self, args, false)
end
end
@@ -199,6 +198,16 @@ module ActionDispatch
if args.size == arg_size && !inner_options && optimize_routes_generation?(t)
options = t.url_options.merge @options
options[:path] = optimized_helper(args)
+
+ original_script_name = options.delete(:original_script_name)
+ script_name = t._routes.find_script_name(options)
+
+ if original_script_name
+ script_name = original_script_name + script_name
+ end
+
+ options[:script_name] = script_name
+
url_strategy.call options
else
super
@@ -584,14 +593,14 @@ module ActionDispatch
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.2.
+ will be removed in Rails 6.0.
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.2.
+ will be removed in Rails 6.0.
MSG
end
@@ -842,6 +851,10 @@ module ActionDispatch
end
req = make_request(env)
+ recognize_path_with_request(req, path, extras)
+ end
+
+ def recognize_path_with_request(req, path, extras, raise_on_missing: true)
@router.recognize(req) do |route, params|
params.merge!(extras)
params.each do |key, value|
@@ -860,10 +873,15 @@ module ActionDispatch
end
return req.path_parameters
+ elsif app.matches?(req) && app.engine?
+ path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras, raise_on_missing: false)
+ return path_parameters if path_parameters
end
end
- raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ if raise_on_missing
+ raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ end
end
end
# :startdoc:
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index 3ae533dd37..1a31c7dbb8 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -155,7 +155,7 @@ module ActionDispatch
# Missing routes keys may be filled in from the current request's parameters
# (e.g. +:controller+, +:action+, +:id+ and any other parameters that are
# placed in the path). Given that the current action has been reached
- # through `GET /users/1`:
+ # through <tt>GET /users/1</tt>:
#
# url_for(only_path: true) # => '/users/1'
# url_for(only_path: true, action: 'edit') # => '/users/1/edit'
@@ -191,7 +191,25 @@ module ActionDispatch
end
end
- def route_for(name, *args) # :nodoc:
+ # Allows calling direct or regular named route.
+ #
+ # resources :buckets
+ #
+ # direct :recordable do |recording|
+ # route_for(:bucket, recording.bucket)
+ # end
+ #
+ # direct :threadable do |threadable|
+ # route_for(:recordable, threadable.parent)
+ # end
+ #
+ # This maintains the context of the original caller on
+ # whether to return a path or full URL, e.g:
+ #
+ # threadable_path(threadable) # => "/buckets/1"
+ # threadable_url(threadable) # => "http://example.com/buckets/1"
+ #
+ def route_for(name, *args)
public_send(:"#{name}_url", *args)
end
diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb
index ae4aeac59d..c74c0ccced 100644
--- a/actionpack/lib/action_dispatch/system_test_case.rb
+++ b/actionpack/lib/action_dispatch/system_test_case.rb
@@ -1,13 +1,16 @@
# frozen_string_literal: true
+gem "capybara", ">= 2.15"
+
require "capybara/dsl"
require "capybara/minitest"
require "action_controller"
-require_relative "system_testing/driver"
-require_relative "system_testing/server"
-require_relative "system_testing/test_helpers/screenshot_helper"
-require_relative "system_testing/test_helpers/setup_and_teardown"
-require_relative "system_testing/test_helpers/undef_methods"
+require "action_dispatch/system_testing/driver"
+require "action_dispatch/system_testing/browser"
+require "action_dispatch/system_testing/server"
+require "action_dispatch/system_testing/test_helpers/screenshot_helper"
+require "action_dispatch/system_testing/test_helpers/setup_and_teardown"
+require "action_dispatch/system_testing/test_helpers/undef_methods"
module ActionDispatch
# = System Testing
@@ -67,6 +70,9 @@ module ActionDispatch
# size of the browser screen. These two options are not applicable for
# headless drivers and will be silently ignored if passed.
#
+ # Headless browsers such as headless Chrome and headless Firefox are also supported.
+ # You can use these browsers by setting the +:using+ argument to +:headless_chrome+ or +:headless_firefox+.
+ #
# To use a headless driver, like Poltergeist, update your Gemfile to use
# Poltergeist instead of Selenium and then declare the driver name in the
# +application_system_test_case.rb+ file. In this case, you would leave out
@@ -119,14 +125,22 @@ module ActionDispatch
#
# driven_by :poltergeist
#
+ # driven_by :selenium, screen_size: [800, 800]
+ #
+ # driven_by :selenium, using: :chrome
+ #
+ # driven_by :selenium, using: :headless_chrome
+ #
# driven_by :selenium, using: :firefox
#
- # driven_by :selenium, screen_size: [800, 800]
+ # driven_by :selenium, using: :headless_firefox
def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {})
self.driver = SystemTesting::Driver.new(driver, using: using, screen_size: screen_size, options: options)
end
driven_by :selenium
+
+ ActiveSupport.run_load_hooks(:action_dispatch_system_test_case, self)
end
SystemTestCase.start_application
diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb
new file mode 100644
index 0000000000..1b0bce6b9e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/browser.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ class Browser # :nodoc:
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def type
+ case name
+ when :headless_chrome
+ :chrome
+ when :headless_firefox
+ :firefox
+ else
+ name
+ end
+ end
+
+ def options
+ case name
+ when :headless_chrome
+ headless_chrome_browser_options
+ when :headless_firefox
+ headless_firefox_browser_options
+ end
+ end
+
+ private
+ def headless_chrome_browser_options
+ options = Selenium::WebDriver::Chrome::Options.new
+ options.args << "--headless"
+ options.args << "--disable-gpu" if Gem.win_platform?
+
+ options
+ end
+
+ def headless_firefox_browser_options
+ options = Selenium::WebDriver::Firefox::Options.new
+ options.args << "-headless"
+
+ options
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb
index 4279336f2f..5252ff6746 100644
--- a/actionpack/lib/action_dispatch/system_testing/driver.rb
+++ b/actionpack/lib/action_dispatch/system_testing/driver.rb
@@ -5,7 +5,7 @@ module ActionDispatch
class Driver # :nodoc:
def initialize(name, **options)
@name = name
- @browser = options[:using]
+ @browser = Browser.new(options[:using])
@screen_size = options[:screen_size]
@options = options[:options]
end
@@ -31,8 +31,12 @@ module ActionDispatch
end
end
+ def browser_options
+ @options.merge(options: @browser.options).compact
+ end
+
def register_selenium(app)
- Capybara::Selenium::Driver.new(app, { browser: @browser }.merge(@options)).tap do |driver|
+ Capybara::Selenium::Driver.new(app, { browser: @browser.type }.merge(browser_options)).tap do |driver|
driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size)
end
end
@@ -43,7 +47,7 @@ module ActionDispatch
def register_webkit(app)
Capybara::Webkit::Driver.new(app, Capybara::Webkit::Configuration.to_hash.merge(@options)).tap do |driver|
- driver.resize_window(*@screen_size)
+ driver.resize_window_to(driver.current_window_handle, *@screen_size)
end
end
diff --git a/actionpack/lib/action_dispatch/system_testing/server.rb b/actionpack/lib/action_dispatch/system_testing/server.rb
index 76bada8df1..4fc1f33767 100644
--- a/actionpack/lib/action_dispatch/system_testing/server.rb
+++ b/actionpack/lib/action_dispatch/system_testing/server.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "rack/handler/puma"
-
module ActionDispatch
module SystemTesting
class Server # :nodoc:
@@ -12,29 +10,17 @@ module ActionDispatch
self.silence_puma = false
def run
- register
setup
end
private
- def register
- Capybara.register_server :rails_puma do |app, port, host|
- Rack::Handler::Puma.run(
- app,
- Port: port,
- Threads: "0:1",
- Silent: self.class.silence_puma
- )
- end
- end
-
def setup
set_server
set_port
end
def set_server
- Capybara.server = :rails_puma
+ Capybara.server = :puma, { Silent: self.class.silence_puma } if Capybara.server == Capybara.servers[:default]
end
def set_port
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
index 6c337cdc31..d2685e0452 100644
--- a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
@@ -15,12 +15,11 @@ module ActionDispatch
#
# You can set the +RAILS_SYSTEM_TESTING_SCREENSHOT+ environment variable to
# control the output. Possible values are:
- # * [+inline+ (default)] display the screenshot in the terminal using the
+ # * [+simple+ (default)] Only displays the screenshot path.
+ # This is the default value.
+ # * [+inline+] Display the screenshot in the terminal using the
# iTerm image protocol (https://iterm2.com/documentation-images.html).
- # * [+simple+] only display the screenshot path.
- # This is the default value if the +CI+ environment variables
- # is defined.
- # * [+artifact+] display the screenshot in the terminal, using the terminal
+ # * [+artifact+] Display the screenshot in the terminal, using the terminal
# artifact format (https://buildkite.github.io/terminal/inline-images/).
def take_screenshot
save_image
@@ -44,7 +43,7 @@ module ActionDispatch
end
def image_path
- @image_path ||= absolute_image_path.relative_path_from(Pathname.pwd).to_s
+ @image_path ||= absolute_image_path.to_s
end
def absolute_image_path
@@ -59,11 +58,8 @@ module ActionDispatch
# Environment variables have priority
output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] || ENV["CAPYBARA_INLINE_SCREENSHOT"]
- # If running in a CI environment, default to simple
- output_type ||= "simple" if ENV["CI"]
-
- # Default
- output_type ||= "inline"
+ # Default to outputting a path to the screenshot
+ output_type ||= "simple"
output_type
end
@@ -84,7 +80,7 @@ module ActionDispatch
end
def inline_base64(path)
- Base64.encode64(path).gsub("\n", "")
+ Base64.strict_encode64(path)
end
def failed?
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
index ffa85f4e14..e47d5020f4 100644
--- a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
@@ -19,6 +19,7 @@ module ActionDispatch
def after_teardown
take_failed_screenshot
Capybara.reset_sessions!
+ ensure
super
end
end
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb
index ef680cafed..d64be3b3d9 100644
--- a/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb
@@ -14,7 +14,7 @@ module ActionDispatch
def method_missing(method, *args, &block)
if METHODS.include?(method)
- raise NoMethodError
+ raise NoMethodError, "System tests cannot make direct requests via ##{method}; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information."
else
super
end
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index ae1f368e8b..7171b6942c 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -7,43 +7,43 @@ require "active_support/core_ext/object/try"
require "rack/test"
require "minitest"
-require_relative "request_encoder"
+require "action_dispatch/testing/request_encoder"
module ActionDispatch
module Integration #:nodoc:
module RequestHelpers
- # Performs a GET request with the given parameters. See +#process+ for more
- # details.
+ # Performs a GET request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
def get(path, **args)
process(:get, path, **args)
end
- # Performs a POST request with the given parameters. See +#process+ for more
- # details.
+ # Performs a POST request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
def post(path, **args)
process(:post, path, **args)
end
- # Performs a PATCH request with the given parameters. See +#process+ for more
- # details.
+ # Performs a PATCH request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
def patch(path, **args)
process(:patch, path, **args)
end
- # Performs a PUT request with the given parameters. See +#process+ for more
- # details.
+ # Performs a PUT request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
def put(path, **args)
process(:put, path, **args)
end
- # Performs a DELETE request with the given parameters. See +#process+ for
- # more details.
+ # Performs a DELETE request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
def delete(path, **args)
process(:delete, path, **args)
end
- # Performs a HEAD request with the given parameters. See +#process+ for more
- # details.
+ # Performs a HEAD request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
def head(path, *args)
process(:head, path, *args)
end
diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb
index 3b63706aaa..8ac50c730d 100644
--- a/actionpack/lib/action_dispatch/testing/test_process.rb
+++ b/actionpack/lib/action_dispatch/testing/test_process.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../middleware/cookies"
-require_relative "../middleware/flash"
+require "action_dispatch/middleware/cookies"
+require "action_dispatch/middleware/flash"
module ActionDispatch
module TestProcess
diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb
index b23ea7479c..1e6b21f235 100644
--- a/actionpack/lib/action_dispatch/testing/test_response.rb
+++ b/actionpack/lib/action_dispatch/testing/test_response.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "request_encoder"
+require "action_dispatch/testing/request_encoder"
module ActionDispatch
# Integration test methods such as ActionDispatch::Integration::Session#get
diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb
index fe2fc7c474..3f69109633 100644
--- a/actionpack/lib/action_pack.rb
+++ b/actionpack/lib/action_pack.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2004-2017 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -23,4 +23,4 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-require_relative "action_pack/version"
+require "action_pack/version"
diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb
index 28bc153f4d..37969fcb57 100644
--- a/actionpack/lib/action_pack/gem_version.rb
+++ b/actionpack/lib/action_pack/gem_version.rb
@@ -7,8 +7,8 @@ module ActionPack
end
module VERSION
- MAJOR = 5
- MINOR = 2
+ MAJOR = 6
+ MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb
index fdc09bd951..4512ea27b3 100644
--- a/actionpack/test/abstract/callbacks_test.rb
+++ b/actionpack/test/abstract/callbacks_test.rb
@@ -154,7 +154,7 @@ module AbstractController
test "when :except is specified, an after action is not triggered on that action" do
@controller.process(:index)
- assert !@controller.instance_variable_defined?("@authenticated")
+ assert_not @controller.instance_variable_defined?("@authenticated")
end
end
@@ -198,7 +198,7 @@ module AbstractController
test "when :except is specified with an array, an after action is not triggered on that action" do
@controller.process(:index)
- assert !@controller.instance_variable_defined?("@authenticated")
+ assert_not @controller.instance_variable_defined?("@authenticated")
end
end
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index caa56018f8..f4787ed27a 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -44,7 +44,7 @@ module Rails
@_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test")
end
- def root; end;
+ def root; end
end
end
@@ -380,10 +380,8 @@ class ForkingExecutor
def initialize(size)
@size = size
@queue = Server.new
- file = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname("rails-tests", "fd")
- @url = "drbunix://#{file}"
@pool = nil
- DRb.start_service @url, @queue
+ @url = DRb.start_service("drbunix:", @queue).uri
end
def <<(work); @queue << work; end
@@ -449,3 +447,11 @@ end
class DrivenBySeleniumWithChrome < ActionDispatch::SystemTestCase
driven_by :selenium, using: :chrome
end
+
+class DrivenBySeleniumWithHeadlessChrome < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :headless_chrome
+end
+
+class DrivenBySeleniumWithHeadlessFirefox < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :headless_firefox
+end
diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb
index f9a037e3cc..763df3a776 100644
--- a/actionpack/test/controller/action_pack_assertions_test.rb
+++ b/actionpack/test/controller/action_pack_assertions_test.rb
@@ -290,29 +290,29 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
def test_template_objects_exist
process :assign_this
- assert !@controller.instance_variable_defined?(:"@hi")
+ assert_not @controller.instance_variable_defined?(:"@hi")
assert @controller.instance_variable_get(:"@howdy")
end
def test_template_objects_missing
process :nothing
- assert !@controller.instance_variable_defined?(:@howdy)
+ assert_not @controller.instance_variable_defined?(:@howdy)
end
def test_empty_flash
process :flash_me_naked
- assert flash.empty?
+ assert_empty flash
end
def test_flash_exist
process :flash_me
- assert flash.any?
- assert flash["hello"].present?
+ assert_predicate flash, :any?
+ assert_predicate flash["hello"], :present?
end
def test_flash_does_not_exist
process :nothing
- assert flash.empty?
+ assert_empty flash
end
def test_session_exist
@@ -322,7 +322,7 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
def session_does_not_exist
process :nothing
- assert session.empty?
+ assert_empty session
end
def test_redirection_location
@@ -343,46 +343,46 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
def test_server_error_response_code
process :response500
- assert @response.server_error?
+ assert_predicate @response, :server_error?
process :response599
- assert @response.server_error?
+ assert_predicate @response, :server_error?
process :response404
- assert !@response.server_error?
+ assert_not_predicate @response, :server_error?
end
def test_missing_response_code
process :response404
- assert @response.not_found?
+ assert_predicate @response, :not_found?
end
def test_client_error_response_code
process :response404
- assert @response.client_error?
+ assert_predicate @response, :client_error?
end
def test_redirect_url_match
process :redirect_external
- assert @response.redirect?
+ assert_predicate @response, :redirect?
assert_match(/rubyonrails/, @response.redirect_url)
- assert !/perloffrails/.match(@response.redirect_url)
+ assert_no_match(/perloffrails/, @response.redirect_url)
end
def test_redirection
process :redirect_internal
- assert @response.redirect?
+ assert_predicate @response, :redirect?
process :redirect_external
- assert @response.redirect?
+ assert_predicate @response, :redirect?
process :nothing
- assert !@response.redirect?
+ assert_not_predicate @response, :redirect?
end
def test_successful_response_code
process :nothing
- assert @response.successful?
+ assert_predicate @response, :successful?
end
def test_response_object
diff --git a/actionpack/test/controller/api/conditional_get_test.rb b/actionpack/test/controller/api/conditional_get_test.rb
index fd1997f26c..e366ce9532 100644
--- a/actionpack/test/controller/api/conditional_get_test.rb
+++ b/actionpack/test/controller/api/conditional_get_test.rb
@@ -53,7 +53,7 @@ class ConditionalGetApiTest < ActionController::TestCase
@request.if_modified_since = @last_modified
get :one
assert_equal 304, @response.status.to_i
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal @last_modified, @response.headers["Last-Modified"]
end
end
diff --git a/actionpack/test/controller/api/force_ssl_test.rb b/actionpack/test/controller/api/force_ssl_test.rb
index 07459c3753..8191578eb0 100644
--- a/actionpack/test/controller/api/force_ssl_test.rb
+++ b/actionpack/test/controller/api/force_ssl_test.rb
@@ -3,7 +3,9 @@
require "abstract_unit"
class ForceSSLApiController < ActionController::API
- force_ssl
+ ActiveSupport::Deprecation.silence do
+ force_ssl
+ end
def one; end
def two
diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb
index 9ac82c0d65..a672ede1a9 100644
--- a/actionpack/test/controller/base_test.rb
+++ b/actionpack/test/controller/base_test.rb
@@ -107,9 +107,9 @@ class ControllerInstanceTests < ActiveSupport::TestCase
end
def test_performed?
- assert !@empty.performed?
+ assert_not_predicate @empty, :performed?
@empty.response_body = ["sweet"]
- assert @empty.performed?
+ assert_predicate @empty, :performed?
end
def test_action_methods
diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb
index e0300539c9..6fe036dd15 100644
--- a/actionpack/test/controller/caching_test.rb
+++ b/actionpack/test/controller/caching_test.rb
@@ -94,14 +94,14 @@ class FragmentCachingTest < ActionController::TestCase
def test_fragment_exist_with_caching_enabled
@store.write("views/name", "value")
assert @controller.fragment_exist?("name")
- assert !@controller.fragment_exist?("other_name")
+ assert_not @controller.fragment_exist?("other_name")
end
def test_fragment_exist_with_caching_disabled
@controller.perform_caching = false
@store.write("views/name", "value")
- assert !@controller.fragment_exist?("name")
- assert !@controller.fragment_exist?("other_name")
+ assert_not @controller.fragment_exist?("name")
+ assert_not @controller.fragment_exist?("other_name")
end
def test_write_fragment_with_caching_enabled
@@ -144,7 +144,7 @@ class FragmentCachingTest < ActionController::TestCase
buffer = "generated till now -> ".html_safe
buffer << view_context.send(:fragment_for, "expensive") { fragment_computed = true }
- assert !fragment_computed
+ assert_not fragment_computed
assert_equal "generated till now -> fragment content", buffer
end
@@ -159,7 +159,7 @@ class FragmentCachingTest < ActionController::TestCase
html_safe = @controller.read_fragment("name")
assert_equal content, html_safe
- assert html_safe.html_safe?
+ assert_predicate html_safe, :html_safe?
end
end
@@ -173,6 +173,9 @@ class FunctionalCachingController < CachingController
end
end
+ def xml_fragment_cached_with_html_partial
+ end
+
def formatted_fragment_cached
respond_to do |format|
format.html
@@ -308,6 +311,11 @@ CACHED
@store.read("views/functional_caching/formatted_fragment_cached_with_variant:#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}/fragment")
end
+ def test_fragment_caching_with_html_partials_in_xml
+ get :xml_fragment_cached_with_html_partial, format: "*/*"
+ assert_response :success
+ end
+
private
def template_digest(name)
ActionView::Digestor.digest(name: name, finder: @controller.lookup_context)
@@ -317,7 +325,7 @@ end
class CacheHelperOutputBufferTest < ActionController::TestCase
class MockController
def read_fragment(name, options)
- return false
+ false
end
def write_fragment(name, fragment, options)
@@ -333,9 +341,9 @@ class CacheHelperOutputBufferTest < ActionController::TestCase
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;
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
end
cache_helper.extend(ActionView::Helpers::CacheHelper)
@@ -354,9 +362,9 @@ class CacheHelperOutputBufferTest < ActionController::TestCase
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;
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
end
cache_helper.extend(ActionView::Helpers::CacheHelper)
@@ -382,7 +390,7 @@ class ViewCacheDependencyTest < ActionController::TestCase
end
def test_view_cache_dependencies_are_empty_by_default
- assert NoDependenciesController.new.view_cache_dependencies.empty?
+ assert_empty NoDependenciesController.new.view_cache_dependencies
end
def test_view_cache_dependencies_are_listed_in_declaration_order
diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb
index 9f0a9dec7a..425a6e25cc 100644
--- a/actionpack/test/controller/filters_test.rb
+++ b/actionpack/test/controller/filters_test.rb
@@ -787,7 +787,7 @@ class FilterTest < ActionController::TestCase
assert_equal %w( ensure_login find_user ), @controller.instance_variable_get(:@ran_filter)
test_process(ConditionalSkippingController, "login")
- assert !@controller.instance_variable_defined?("@ran_after_action")
+ assert_not @controller.instance_variable_defined?("@ran_after_action")
test_process(ConditionalSkippingController, "change_password")
assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_action")
end
@@ -819,7 +819,7 @@ class FilterTest < ActionController::TestCase
response = test_process(RescuedController)
end
- assert response.successful?
+ assert_predicate response, :successful?
assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body)
end
diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb
index f31a4d9329..e3ec5bb7fc 100644
--- a/actionpack/test/controller/flash_hash_test.rb
+++ b/actionpack/test/controller/flash_hash_test.rb
@@ -44,7 +44,7 @@ module ActionDispatch
@hash["foo"] = "bar"
@hash.delete "foo"
- assert !@hash.key?("foo")
+ assert_not @hash.key?("foo")
assert_nil @hash["foo"]
end
@@ -53,7 +53,7 @@ module ActionDispatch
assert_equal({ "foo" => "bar" }, @hash.to_hash)
@hash.to_hash["zomg"] = "aaron"
- assert !@hash.key?("zomg")
+ assert_not @hash.key?("zomg")
assert_equal({ "foo" => "bar" }, @hash.to_hash)
end
@@ -92,11 +92,11 @@ module ActionDispatch
end
def test_empty?
- assert @hash.empty?
+ assert_empty @hash
@hash["zomg"] = "bears"
- assert !@hash.empty?
+ assert_not_empty @hash
@hash.clear
- assert @hash.empty?
+ assert_empty @hash
end
def test_each
diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb
index d92ae0b817..34bc2c0caa 100644
--- a/actionpack/test/controller/flash_test.rb
+++ b/actionpack/test/controller/flash_test.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "abstract_unit"
-require "active_support/key_generator"
+require "active_support/messages/rotation_configuration"
class FlashTest < ActionController::TestCase
class TestController < ActionController::Base
@@ -243,6 +243,7 @@ end
class FlashIntegrationTest < ActionDispatch::IntegrationTest
SessionKey = "_myapp_session"
Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33")
+ Rotations = ActiveSupport::Messages::RotationConfiguration.new
class TestController < ActionController::Base
add_flash_types :bar
@@ -348,6 +349,7 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest
args[0] ||= {}
args[0][:env] ||= {}
args[0][:env]["action_dispatch.key_generator"] ||= Generator
+ args[0][:env]["action_dispatch.cookies_rotations"] = Rotations
super(path, *args)
end
diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb
index 84ac1fda3c..7f59f6acaf 100644
--- a/actionpack/test/controller/force_ssl_test.rb
+++ b/actionpack/test/controller/force_ssl_test.rb
@@ -13,19 +13,23 @@ class ForceSSLController < ActionController::Base
end
class ForceSSLControllerLevel < ForceSSLController
- force_ssl
+ ActiveSupport::Deprecation.silence do
+ force_ssl
+ end
end
class ForceSSLCustomOptions < ForceSSLController
- force_ssl host: "secure.example.com", only: :redirect_host
- force_ssl port: 8443, only: :redirect_port
- force_ssl subdomain: "secure", only: :redirect_subdomain
- force_ssl domain: "secure.com", only: :redirect_domain
- force_ssl path: "/foo", only: :redirect_path
- force_ssl status: :found, only: :redirect_status
- force_ssl flash: { message: "Foo, Bar!" }, only: :redirect_flash
- force_ssl alert: "Foo, Bar!", only: :redirect_alert
- force_ssl notice: "Foo, Bar!", only: :redirect_notice
+ ActiveSupport::Deprecation.silence do
+ force_ssl host: "secure.example.com", only: :redirect_host
+ force_ssl port: 8443, only: :redirect_port
+ force_ssl subdomain: "secure", only: :redirect_subdomain
+ force_ssl domain: "secure.com", only: :redirect_domain
+ force_ssl path: "/foo", only: :redirect_path
+ force_ssl status: :found, only: :redirect_status
+ force_ssl flash: { message: "Foo, Bar!" }, only: :redirect_flash
+ force_ssl alert: "Foo, Bar!", only: :redirect_alert
+ force_ssl notice: "Foo, Bar!", only: :redirect_notice
+ end
def force_ssl_action
render plain: action_name
@@ -55,15 +59,21 @@ class ForceSSLCustomOptions < ForceSSLController
end
class ForceSSLOnlyAction < ForceSSLController
- force_ssl only: :cheeseburger
+ ActiveSupport::Deprecation.silence do
+ force_ssl only: :cheeseburger
+ end
end
class ForceSSLExceptAction < ForceSSLController
- force_ssl except: :banana
+ ActiveSupport::Deprecation.silence do
+ force_ssl except: :banana
+ end
end
class ForceSSLIfCondition < ForceSSLController
- force_ssl if: :use_force_ssl?
+ ActiveSupport::Deprecation.silence do
+ force_ssl if: :use_force_ssl?
+ end
def use_force_ssl?
action_name == "cheeseburger"
@@ -71,7 +81,9 @@ class ForceSSLIfCondition < ForceSSLController
end
class ForceSSLFlash < ForceSSLController
- force_ssl except: [:banana, :set_flash, :use_flash]
+ ActiveSupport::Deprecation.silence do
+ force_ssl except: [:banana, :set_flash, :use_flash]
+ end
def set_flash
flash["that"] = "hello"
diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb
index 76ff784926..3f211cd60d 100644
--- a/actionpack/test/controller/http_digest_authentication_test.rb
+++ b/actionpack/test/controller/http_digest_authentication_test.rb
@@ -9,7 +9,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
before_action :authenticate_with_request, only: :display
USERS = { "lifo" => "world", "pretty" => "please",
- "dhh" => ::Digest::MD5::hexdigest(["dhh", "SuperSecret", "secret"].join(":")) }
+ "dhh" => ::Digest::MD5.hexdigest(["dhh", "SuperSecret", "secret"].join(":")) }
def index
render plain: "Hello Secret"
@@ -181,9 +181,10 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
end
test "authentication request with password stored as ha1 digest hash" do
- @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "dhh",
- password: ::Digest::MD5::hexdigest(["dhh", "SuperSecret", "secret"].join(":")),
- password_is_ha1: true)
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(
+ username: "dhh",
+ password: ::Digest::MD5.hexdigest(["dhh", "SuperSecret", "secret"].join(":")),
+ password_is_ha1: true)
get :display
assert_response :success
@@ -201,7 +202,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
test "validate_digest_response should fail with nil returning password_procedure" do
@request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: nil, password: nil)
- assert !ActionController::HttpAuthentication::Digest.validate_digest_response(@request, "SuperSecret") { nil }
+ assert_not ActionController::HttpAuthentication::Digest.validate_digest_response(@request, "SuperSecret") { nil }
end
test "authentication request with request-uri ending in '/'" do
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index fd1c5e693f..9cdf04b886 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -14,11 +14,11 @@ class SessionTest < ActiveSupport::TestCase
end
def test_https_bang_works_and_sets_truth_by_default
- assert !@session.https?
+ assert_not_predicate @session, :https?
@session.https!
- assert @session.https?
+ assert_predicate @session, :https?
@session.https! false
- assert !@session.https?
+ assert_not_predicate @session, :https?
end
def test_host!
@@ -135,7 +135,7 @@ class IntegrationTestTest < ActiveSupport::TestCase
session1 = @test.open_session { |sess| }
session2 = @test.open_session # implicit session
- assert !session1.equal?(session2)
+ assert_not session1.equal?(session2)
end
# RSpec mixes Matchers (which has a #method_missing) into
@@ -345,7 +345,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
follow_redirect!
assert_response :ok
- refute_same previous_html_document, html_document
+ assert_not_same previous_html_document, html_document
end
end
@@ -375,7 +375,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
a = open_session
b = open_session
- refute_same(a.integration_session, b.integration_session)
+ assert_not_same(a.integration_session, b.integration_session)
end
def test_get_with_query_string
@@ -412,11 +412,11 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
get "/get_with_params", params: { foo: "bar" }
- assert request.env["rack.input"].string.empty?
+ assert_empty request.env["rack.input"].string
assert_equal "foo=bar", request.env["QUERY_STRING"]
assert_equal "foo=bar", request.query_string
assert_equal "bar", request.parameters["foo"]
- assert request.parameters["leaks"].nil?
+ assert_predicate request.parameters["leaks"], :nil?
end
end
diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb
index 8cfb43a6bc..431fe90b23 100644
--- a/actionpack/test/controller/live_stream_test.rb
+++ b/actionpack/test/controller/live_stream_test.rb
@@ -464,7 +464,7 @@ module ActionController
end
def test_stale_with_etag
- @request.if_none_match = %(W/"#{Digest::MD5.hexdigest('123')}")
+ @request.if_none_match = %(W/"#{ActiveSupport::Digest.hexdigest('123')}")
get :with_stale
assert_equal 304, response.status.to_i
end
diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb
index f0f106c8ba..be455642de 100644
--- a/actionpack/test/controller/log_subscriber_test.rb
+++ b/actionpack/test/controller/log_subscriber_test.rb
@@ -98,7 +98,7 @@ class ACLogSubscriberTest < ActionController::TestCase
@old_logger = ActionController::Base.logger
- @cache_path = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname("tmp", "cache")
+ @cache_path = Dir.mktmpdir(%w[tmp cache])
@controller.cache_store = :file_store, @cache_path
ActionController::LogSubscriber.attach_to :action_controller
end
diff --git a/actionpack/test/controller/metal_test.rb b/actionpack/test/controller/metal_test.rb
index c235c9df86..248ef36b7c 100644
--- a/actionpack/test/controller/metal_test.rb
+++ b/actionpack/test/controller/metal_test.rb
@@ -9,7 +9,7 @@ class MetalControllerInstanceTests < ActiveSupport::TestCase
end
end
- def test_response_has_default_headers
+ def test_response_does_not_have_default_headers
original_default_headers = ActionDispatch::Response.default_headers
ActionDispatch::Response.default_headers = {
@@ -23,9 +23,9 @@ class MetalControllerInstanceTests < ActiveSupport::TestCase
"rack.input" => -> {}
)[1]
- refute response_headers.key?("X-Frame-Options")
- refute response_headers.key?("X-Content-Type-Options")
- refute response_headers.key?("X-XSS-Protection")
+ assert_not response_headers.key?("X-Frame-Options")
+ assert_not response_headers.key?("X-Content-Type-Options")
+ assert_not response_headers.key?("X-XSS-Protection")
ensure
ActionDispatch::Response.default_headers = original_default_headers
end
diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb
index f9ffd5f54c..771eccb29b 100644
--- a/actionpack/test/controller/mime/respond_to_test.rb
+++ b/actionpack/test/controller/mime/respond_to_test.rb
@@ -658,13 +658,13 @@ class RespondToControllerTest < ActionController::TestCase
end
def test_variant_without_implicit_rendering_from_browser
- assert_raises(ActionController::UnknownFormat) do
+ assert_raises(ActionController::MissingExactTemplate) 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
+ assert_raises(ActionController::MissingExactTemplate) do
get :variant_without_implicit_template_rendering
end
end
diff --git a/actionpack/test/controller/new_base/base_test.rb b/actionpack/test/controller/new_base/base_test.rb
index d9f200b2a7..280134f8d2 100644
--- a/actionpack/test/controller/new_base/base_test.rb
+++ b/actionpack/test/controller/new_base/base_test.rb
@@ -47,7 +47,6 @@ module Dispatching
end
class BaseTest < Rack::TestCase
- # :api: plugin
test "simple dispatching" do
get "/dispatching/simple/index"
@@ -56,14 +55,12 @@ module Dispatching
assert_content_type "text/plain; charset=utf-8"
end
- # :api: plugin
test "directly modifying response body" do
get "/dispatching/simple/modify_response_body"
assert_body "success"
end
- # :api: plugin
test "directly modifying response body twice" do
get "/dispatching/simple/modify_response_body_twice"
diff --git a/actionpack/test/controller/output_escaping_test.rb b/actionpack/test/controller/output_escaping_test.rb
index e33a99068f..d683bc73e6 100644
--- a/actionpack/test/controller/output_escaping_test.rb
+++ b/actionpack/test/controller/output_escaping_test.rb
@@ -4,7 +4,7 @@ require "abstract_unit"
class OutputEscapingTest < ActiveSupport::TestCase
test "escape_html shouldn't die when passed nil" do
- assert ERB::Util.h(nil).blank?
+ assert_predicate ERB::Util.h(nil), :blank?
end
test "escapeHTML should escape strings" do
diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb
index 43cabae7d2..674b2c6266 100644
--- a/actionpack/test/controller/parameters/accessors_test.rb
+++ b/actionpack/test/controller/parameters/accessors_test.rb
@@ -2,7 +2,6 @@
require "abstract_unit"
require "action_controller/metal/strong_parameters"
-require "active_support/core_ext/hash/transform_values"
class ParametersAccessorsTest < ActiveSupport::TestCase
setup do
@@ -22,13 +21,13 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
test "[] retains permitted status" do
@params.permit!
- assert @params[:person].permitted?
- assert @params[:person][:name].permitted?
+ assert_predicate @params[:person], :permitted?
+ assert_predicate @params[:person][:name], :permitted?
end
test "[] retains unpermitted status" do
- assert_not @params[:person].permitted?
- assert_not @params[:person][:name].permitted?
+ assert_not_predicate @params[:person], :permitted?
+ assert_not_predicate @params[:person][:name], :permitted?
end
test "as_json returns the JSON representation of the parameters hash" do
@@ -51,6 +50,14 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
@params.each { |key, value| assert_not(value.permitted?) if key == "person" }
end
+ test "each returns key,value array for block with arity 1" do
+ @params.each do |arg|
+ assert_kind_of Array, arg
+ assert_equal "person", arg[0]
+ assert_kind_of ActionController::Parameters, arg[1]
+ end
+ end
+
test "each_pair carries permitted status" do
@params.permit!
@params.each_pair { |key, value| assert(value.permitted?) if key == "person" }
@@ -60,35 +67,43 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
@params.each_pair { |key, value| assert_not(value.permitted?) if key == "person" }
end
+ test "each_pair returns key,value array for block with arity 1" do
+ @params.each_pair do |arg|
+ assert_kind_of Array, arg
+ assert_equal "person", arg[0]
+ assert_kind_of ActionController::Parameters, arg[1]
+ end
+ end
+
test "empty? returns true when params contains no key/value pairs" do
params = ActionController::Parameters.new
- assert params.empty?
+ assert_empty params
end
test "empty? returns false when any params are present" do
- refute @params.empty?
+ assert_not_empty @params
end
test "except retains permitted status" do
@params.permit!
- assert @params.except(:person).permitted?
- assert @params[:person].except(:name).permitted?
+ assert_predicate @params.except(:person), :permitted?
+ assert_predicate @params[:person].except(:name), :permitted?
end
test "except retains unpermitted status" do
- assert_not @params.except(:person).permitted?
- assert_not @params[:person].except(:name).permitted?
+ assert_not_predicate @params.except(:person), :permitted?
+ assert_not_predicate @params[:person].except(:name), :permitted?
end
test "fetch retains permitted status" do
@params.permit!
- assert @params.fetch(:person).permitted?
- assert @params[:person].fetch(:name).permitted?
+ assert_predicate @params.fetch(:person), :permitted?
+ assert_predicate @params[:person].fetch(:name), :permitted?
end
test "fetch retains unpermitted status" do
- assert_not @params.fetch(:person).permitted?
- assert_not @params[:person].fetch(:name).permitted?
+ assert_not_predicate @params.fetch(:person), :permitted?
+ assert_not_predicate @params[:person].fetch(:name), :permitted?
end
test "has_key? returns true if the given key is present in the params" do
@@ -96,7 +111,7 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
end
test "has_key? returns false if the given key is not present in the params" do
- refute @params.has_key?(:address)
+ assert_not @params.has_key?(:address)
end
test "has_value? returns true if the given value is present in the params" do
@@ -106,7 +121,7 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
test "has_value? returns false if the given value is not present in the params" do
params = ActionController::Parameters.new(city: "Chicago", state: "Illinois")
- refute params.has_value?("New York")
+ assert_not params.has_value?("New York")
end
test "include? returns true if the given key is present in the params" do
@@ -114,7 +129,7 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
end
test "include? returns false if the given key is not present in the params" do
- refute @params.include?(:address)
+ assert_not @params.include?(:address)
end
test "key? returns true if the given key is present in the params" do
@@ -122,7 +137,7 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
end
test "key? returns false if the given key is not present in the params" do
- refute @params.key?(:address)
+ assert_not @params.key?(:address)
end
test "keys returns an array of the keys of the params" do
@@ -131,48 +146,48 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
end
test "reject retains permitted status" do
- assert_not @params.reject { |k| k == "person" }.permitted?
+ assert_not_predicate @params.reject { |k| k == "person" }, :permitted?
end
test "reject retains unpermitted status" do
@params.permit!
- assert @params.reject { |k| k == "person" }.permitted?
+ assert_predicate @params.reject { |k| k == "person" }, :permitted?
end
test "select retains permitted status" do
@params.permit!
- assert @params.select { |k| k == "person" }.permitted?
+ assert_predicate @params.select { |k| k == "person" }, :permitted?
end
test "select retains unpermitted status" do
- assert_not @params.select { |k| k == "person" }.permitted?
+ assert_not_predicate @params.select { |k| k == "person" }, :permitted?
end
test "slice retains permitted status" do
@params.permit!
- assert @params.slice(:person).permitted?
+ assert_predicate @params.slice(:person), :permitted?
end
test "slice retains unpermitted status" do
- assert_not @params.slice(:person).permitted?
+ assert_not_predicate @params.slice(:person), :permitted?
end
test "transform_keys retains permitted status" do
@params.permit!
- assert @params.transform_keys { |k| k }.permitted?
+ assert_predicate @params.transform_keys { |k| k }, :permitted?
end
test "transform_keys retains unpermitted status" do
- assert_not @params.transform_keys { |k| k }.permitted?
+ assert_not_predicate @params.transform_keys { |k| k }, :permitted?
end
test "transform_values retains permitted status" do
@params.permit!
- assert @params.transform_values { |v| v }.permitted?
+ assert_predicate @params.transform_values { |v| v }, :permitted?
end
test "transform_values retains unpermitted status" do
- assert_not @params.transform_values { |v| v }.permitted?
+ assert_not_predicate @params.transform_values { |v| v }, :permitted?
end
test "value? returns true if the given value is present in the params" do
@@ -182,7 +197,7 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
test "value? returns false if the given value is not present in the params" do
params = ActionController::Parameters.new(city: "Chicago", state: "Illinois")
- refute params.value?("New York")
+ assert_not params.value?("New York")
end
test "values returns an array of the values of the params" do
@@ -192,13 +207,13 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
test "values_at retains permitted status" do
@params.permit!
- assert @params.values_at(:person).first.permitted?
- assert @params[:person].values_at(:name).first.permitted?
+ assert_predicate @params.values_at(:person).first, :permitted?
+ assert_predicate @params[:person].values_at(:name).first, :permitted?
end
test "values_at retains unpermitted status" do
- assert_not @params.values_at(:person).first.permitted?
- assert_not @params[:person].values_at(:name).first.permitted?
+ assert_not_predicate @params.values_at(:person).first, :permitted?
+ assert_not_predicate @params[:person].values_at(:name).first, :permitted?
end
test "is equal to Parameters instance with same params" do
@@ -257,23 +272,24 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
assert_match(/permitted: true/, @params.inspect)
end
- if Hash.method_defined?(:dig)
- test "#dig delegates the dig method to its values" do
- assert_equal "David", @params.dig(:person, :name, :first)
- assert_equal "Chicago", @params.dig(:person, :addresses, 0, :city)
- end
+ test "#dig delegates the dig method to its values" do
+ assert_equal "David", @params.dig(:person, :name, :first)
+ assert_equal "Chicago", @params.dig(:person, :addresses, 0, :city)
+ end
- test "#dig converts hashes to parameters" do
- assert_kind_of ActionController::Parameters, @params.dig(:person)
- assert_kind_of ActionController::Parameters, @params.dig(:person, :addresses, 0)
- assert @params.dig(:person, :addresses).all? do |value|
- value.is_a?(ActionController::Parameters)
- end
- end
- else
- test "ActionController::Parameters does not respond to #dig on Ruby 2.2" do
- assert_not ActionController::Parameters.method_defined?(:dig)
- assert_not @params.respond_to?(:dig)
+ test "#dig converts hashes to parameters" do
+ assert_kind_of ActionController::Parameters, @params.dig(:person)
+ assert_kind_of ActionController::Parameters, @params.dig(:person, :addresses, 0)
+ assert @params.dig(:person, :addresses).all? do |value|
+ value.is_a?(ActionController::Parameters)
end
end
+
+ test "mutating #dig return value mutates underlying parameters" do
+ @params.dig(:person, :name)[:first] = "Bill"
+ assert_equal "Bill", @params.dig(:person, :name, :first)
+
+ @params.dig(:person, :addresses)[0] = { city: "Boston", state: "Massachusetts" }
+ assert_equal "Boston", @params.dig(:person, :addresses, 0, :city)
+ 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 1e8b71d789..fe0e5e368d 100644
--- a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
+++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
@@ -25,6 +25,6 @@ class AlwaysPermittedParametersTest < ActiveSupport::TestCase
book: { pages: 65 },
format: "json")
permitted = params.permit book: [:pages]
- assert permitted.permitted?
+ assert_predicate permitted, :permitted?
end
end
diff --git a/actionpack/test/controller/parameters/dup_test.rb b/actionpack/test/controller/parameters/dup_test.rb
index f5833aff46..5403fc6d93 100644
--- a/actionpack/test/controller/parameters/dup_test.rb
+++ b/actionpack/test/controller/parameters/dup_test.rb
@@ -23,7 +23,7 @@ class ParametersDupTest < ActiveSupport::TestCase
test "a duplicate maintains the original's permitted status" do
@params.permit!
dupped_params = @params.dup
- assert dupped_params.permitted?
+ assert_predicate dupped_params, :permitted?
end
test "a duplicate maintains the original's parameters" do
@@ -57,11 +57,11 @@ class ParametersDupTest < ActiveSupport::TestCase
dupped_params = @params.deep_dup
dupped_params.permit!
- assert_not @params.permitted?
+ assert_not_predicate @params, :permitted?
end
test "deep_dup @permitted is being copied" do
@params.permit!
- assert @params.deep_dup.permitted?
+ assert_predicate @params.deep_dup, :permitted?
end
end
diff --git a/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb b/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb
index dcf848a620..c890839727 100644
--- a/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb
+++ b/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb
@@ -21,7 +21,7 @@ class MultiParameterAttributesTest < ActiveSupport::TestCase
permitted = params.permit book: [ :shipped_at, :price ]
- assert permitted.permitted?
+ assert_predicate permitted, :permitted?
assert_equal "2012", permitted[:book]["shipped_at(1i)"]
assert_equal "3", permitted[:book]["shipped_at(2i)"]
diff --git a/actionpack/test/controller/parameters/mutators_test.rb b/actionpack/test/controller/parameters/mutators_test.rb
index 49dede03c2..312b1e5b27 100644
--- a/actionpack/test/controller/parameters/mutators_test.rb
+++ b/actionpack/test/controller/parameters/mutators_test.rb
@@ -2,7 +2,6 @@
require "abstract_unit"
require "action_controller/metal/strong_parameters"
-require "active_support/core_ext/hash/transform_values"
class ParametersMutatorsTest < ActiveSupport::TestCase
setup do
@@ -20,11 +19,11 @@ class ParametersMutatorsTest < ActiveSupport::TestCase
test "delete retains permitted status" do
@params.permit!
- assert @params.delete(:person).permitted?
+ assert_predicate @params.delete(:person), :permitted?
end
test "delete retains unpermitted status" do
- assert_not @params.delete(:person).permitted?
+ assert_not_predicate @params.delete(:person), :permitted?
end
test "delete returns the value when the key is present" do
@@ -50,73 +49,73 @@ class ParametersMutatorsTest < ActiveSupport::TestCase
test "delete_if retains permitted status" do
@params.permit!
- assert @params.delete_if { |k| k == "person" }.permitted?
+ assert_predicate @params.delete_if { |k| k == "person" }, :permitted?
end
test "delete_if retains unpermitted status" do
- assert_not @params.delete_if { |k| k == "person" }.permitted?
+ assert_not_predicate @params.delete_if { |k| k == "person" }, :permitted?
end
test "extract! retains permitted status" do
@params.permit!
- assert @params.extract!(:person).permitted?
+ assert_predicate @params.extract!(:person), :permitted?
end
test "extract! retains unpermitted status" do
- assert_not @params.extract!(:person).permitted?
+ assert_not_predicate @params.extract!(:person), :permitted?
end
test "keep_if retains permitted status" do
@params.permit!
- assert @params.keep_if { |k, v| k == "person" }.permitted?
+ assert_predicate @params.keep_if { |k, v| k == "person" }, :permitted?
end
test "keep_if retains unpermitted status" do
- assert_not @params.keep_if { |k, v| k == "person" }.permitted?
+ assert_not_predicate @params.keep_if { |k, v| k == "person" }, :permitted?
end
test "reject! retains permitted status" do
@params.permit!
- assert @params.reject! { |k| k == "person" }.permitted?
+ assert_predicate @params.reject! { |k| k == "person" }, :permitted?
end
test "reject! retains unpermitted status" do
- assert_not @params.reject! { |k| k == "person" }.permitted?
+ assert_not_predicate @params.reject! { |k| k == "person" }, :permitted?
end
test "select! retains permitted status" do
@params.permit!
- assert @params.select! { |k| k != "person" }.permitted?
+ assert_predicate @params.select! { |k| k != "person" }, :permitted?
end
test "select! retains unpermitted status" do
- assert_not @params.select! { |k| k != "person" }.permitted?
+ assert_not_predicate @params.select! { |k| k != "person" }, :permitted?
end
test "slice! retains permitted status" do
@params.permit!
- assert @params.slice!(:person).permitted?
+ assert_predicate @params.slice!(:person), :permitted?
end
test "slice! retains unpermitted status" do
- assert_not @params.slice!(:person).permitted?
+ assert_not_predicate @params.slice!(:person), :permitted?
end
test "transform_keys! retains permitted status" do
@params.permit!
- assert @params.transform_keys! { |k| k }.permitted?
+ assert_predicate @params.transform_keys! { |k| k }, :permitted?
end
test "transform_keys! retains unpermitted status" do
- assert_not @params.transform_keys! { |k| k }.permitted?
+ assert_not_predicate @params.transform_keys! { |k| k }, :permitted?
end
test "transform_values! retains permitted status" do
@params.permit!
- assert @params.transform_values! { |v| v }.permitted?
+ assert_predicate @params.transform_values! { |v| v }, :permitted?
end
test "transform_values! retains unpermitted status" do
- assert_not @params.transform_values! { |v| v }.permitted?
+ assert_not_predicate @params.transform_values! { |v| v }, :permitted?
end
end
diff --git a/actionpack/test/controller/parameters/nested_parameters_permit_test.rb b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb
index c9fcc483ee..ccc6bf9807 100644
--- a/actionpack/test/controller/parameters/nested_parameters_permit_test.rb
+++ b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb
@@ -32,7 +32,7 @@ class NestedParametersPermitTest < ActiveSupport::TestCase
permitted = params.permit book: [ :title, { authors: [ :name ] }, { details: :pages }, :id ]
- assert permitted.permitted?
+ assert_predicate permitted, :permitted?
assert_equal "Romeo and Juliet", permitted[:book][:title]
assert_equal "William Shakespeare", permitted[:book][:authors][0][:name]
assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name]
diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb
index ebdaca0162..34b9ac0ab8 100644
--- a/actionpack/test/controller/parameters/parameters_permit_test.rb
+++ b/actionpack/test/controller/parameters/parameters_permit_test.rb
@@ -53,13 +53,13 @@ class ParametersPermitTest < ActiveSupport::TestCase
test "if nothing is permitted, the hash becomes empty" do
params = ActionController::Parameters.new(id: "1234")
permitted = params.permit
- assert permitted.permitted?
- assert permitted.empty?
+ assert_predicate permitted, :permitted?
+ assert_empty permitted
end
test "key: permitted scalar values" do
values = ["a", :a, nil]
- values += [0, 1.0, 2**128, BigDecimal.new(1)]
+ values += [0, 1.0, 2**128, BigDecimal(1)]
values += [true, false]
values += [Date.today, Time.now, DateTime.now]
values += [STDOUT, StringIO.new, ActionDispatch::Http::UploadedFile.new(tempfile: __FILE__),
@@ -136,7 +136,7 @@ class ParametersPermitTest < ActiveSupport::TestCase
test "key: it is not assigned if not present in params" do
params = ActionController::Parameters.new(name: "Joe")
permitted = params.permit(:id)
- assert !permitted.has_key?(:id)
+ assert_not permitted.has_key?(:id)
end
test "key to empty array: empty arrays pass" do
@@ -227,7 +227,7 @@ class ParametersPermitTest < ActiveSupport::TestCase
test "hashes in array values get wrapped" do
params = ActionController::Parameters.new(foo: [{}, {}])
params[:foo].each do |hash|
- assert !hash.permitted?
+ assert_not_predicate hash, :permitted?
end
end
@@ -250,7 +250,7 @@ class ParametersPermitTest < ActiveSupport::TestCase
permitted = params.permit(users: [:id])
permitted[:users] << { injected: 1 }
- assert_not permitted[:users].last.permitted?
+ assert_not_predicate permitted[:users].last, :permitted?
end
test "fetch doesnt raise ParameterMissing exception if there is a default" do
@@ -272,12 +272,12 @@ class ParametersPermitTest < ActiveSupport::TestCase
end
test "not permitted is sticky beyond merges" do
- assert !@params.merge(a: "b").permitted?
+ assert_not_predicate @params.merge(a: "b"), :permitted?
end
test "permitted is sticky beyond merges" do
@params.permit!
- assert @params.merge(a: "b").permitted?
+ assert_predicate @params.merge(a: "b"), :permitted?
end
test "merge with parameters" do
@@ -288,12 +288,12 @@ class ParametersPermitTest < ActiveSupport::TestCase
end
test "not permitted is sticky beyond merge!" do
- assert_not @params.merge!(a: "b").permitted?
+ assert_not_predicate @params.merge!(a: "b"), :permitted?
end
test "permitted is sticky beyond merge!" do
@params.permit!
- assert @params.merge!(a: "b").permitted?
+ assert_predicate @params.merge!(a: "b"), :permitted?
end
test "merge! with parameters" do
@@ -309,7 +309,7 @@ class ParametersPermitTest < ActiveSupport::TestCase
merged_params = @params.reverse_merge(default_params)
assert_equal "1234", merged_params[:id]
- refute_predicate merged_params[:person], :empty?
+ assert_not_predicate merged_params[:person], :empty?
end
test "#with_defaults is an alias of reverse_merge" do
@@ -317,11 +317,11 @@ class ParametersPermitTest < ActiveSupport::TestCase
merged_params = @params.with_defaults(default_params)
assert_equal "1234", merged_params[:id]
- refute_predicate merged_params[:person], :empty?
+ assert_not_predicate merged_params[:person], :empty?
end
test "not permitted is sticky beyond reverse_merge" do
- refute_predicate @params.reverse_merge(a: "b"), :permitted?
+ assert_not_predicate @params.reverse_merge(a: "b"), :permitted?
end
test "permitted is sticky beyond reverse_merge" do
@@ -334,7 +334,7 @@ class ParametersPermitTest < ActiveSupport::TestCase
@params.reverse_merge!(default_params)
assert_equal "1234", @params[:id]
- refute_predicate @params[:person], :empty?
+ assert_not_predicate @params[:person], :empty?
end
test "#with_defaults! is an alias of reverse_merge!" do
@@ -342,7 +342,7 @@ class ParametersPermitTest < ActiveSupport::TestCase
@params.with_defaults!(default_params)
assert_equal "1234", @params[:id]
- refute_predicate @params[:person], :empty?
+ assert_not_predicate @params[:person], :empty?
end
test "modifying the parameters" do
@@ -353,12 +353,15 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert_equal "Jonas", @params[:person][:family][:brother]
end
- test "permit is recursive" do
+ test "permit! is recursive" do
+ @params[:nested_array] = [[{ x: 2, y: 3 }, { x: 21, y: 42 }]]
@params.permit!
- assert @params.permitted?
- assert @params[:person].permitted?
- assert @params[:person][:name].permitted?
- assert @params[:person][:addresses][0].permitted?
+ assert_predicate @params, :permitted?
+ assert_predicate @params[:person], :permitted?
+ assert_predicate @params[:person][:name], :permitted?
+ assert_predicate @params[:person][:addresses][0], :permitted?
+ assert_predicate @params[:nested_array][0][0], :permitted?
+ assert_predicate @params[:nested_array][0][1], :permitted?
end
test "permitted takes a default value when Parameters.permit_all_parameters is set" do
@@ -368,8 +371,8 @@ class ParametersPermitTest < ActiveSupport::TestCase
age: "32", name: { first: "David", last: "Heinemeier Hansson" }
})
- assert params.slice(:person).permitted?
- assert params[:person][:name].permitted?
+ assert_predicate params.slice(:person), :permitted?
+ assert_predicate params[:person][:name], :permitted?
ensure
ActionController::Parameters.permit_all_parameters = false
end
@@ -500,9 +503,9 @@ class ParametersPermitTest < ActiveSupport::TestCase
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)
+ assert_not params.permit(foo: []).has_key?(:foo)
+ assert_not params.permit(foo: [:bar]).has_key?(:foo)
+ assert_not params.permit(foo: :bar).has_key?(:foo)
end
test "#permitted? is false by default" do
diff --git a/actionpack/test/controller/parameters/serialization_test.rb b/actionpack/test/controller/parameters/serialization_test.rb
index 823f01d82a..7708c8e4fe 100644
--- a/actionpack/test/controller/parameters/serialization_test.rb
+++ b/actionpack/test/controller/parameters/serialization_test.rb
@@ -2,7 +2,6 @@
require "abstract_unit"
require "action_controller/metal/strong_parameters"
-require "active_support/core_ext/string/strip"
class ParametersSerializationTest < ActiveSupport::TestCase
setup do
@@ -27,21 +26,21 @@ class ParametersSerializationTest < ActiveSupport::TestCase
roundtripped = YAML.load(YAML.dump(params))
assert_equal params, roundtripped
- assert_not roundtripped.permitted?
+ assert_not_predicate roundtripped, :permitted?
end
test "yaml backwardscompatible with psych 2.0.8 format" do
- params = YAML.load <<-end_of_yaml.strip_heredoc
+ params = YAML.load <<~end_of_yaml
--- !ruby/hash:ActionController::Parameters
key: :value
end_of_yaml
assert_equal :value, params[:key]
- assert_not params.permitted?
+ assert_not_predicate params, :permitted?
end
test "yaml backwardscompatible with psych 2.0.9+ format" do
- params = YAML.load(<<-end_of_yaml.strip_heredoc)
+ params = YAML.load(<<~end_of_yaml)
--- !ruby/hash-with-ivars:ActionController::Parameters
elements:
key: :value
@@ -50,6 +49,6 @@ class ParametersSerializationTest < ActiveSupport::TestCase
end_of_yaml
assert_equal :value, params[:key]
- assert_not params.permitted?
+ assert_not_predicate params, :permitted?
end
end
diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb
index df68ef25a3..c4c74e8f2b 100644
--- a/actionpack/test/controller/params_wrapper_test.rb
+++ b/actionpack/test/controller/params_wrapper_test.rb
@@ -255,6 +255,20 @@ class ParamsWrapperTest < ActionController::TestCase
assert_equal "", @response.body
end
end
+
+ def test_derived_wrapped_keys_from_nested_attributes
+ def User.nested_attributes_options
+ { person: {} }
+ end
+
+ assert_called(User, :attribute_names, times: 2, returns: ["username"]) do
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "person_attributes" => { "title" => "Developer" } }
+ assert_parameters("username" => "sikachu", "person_attributes" => { "title" => "Developer" }, "user" => { "username" => "sikachu", "person_attributes" => { "title" => "Developer" } })
+ end
+ end
+ end
end
class NamespacedParamsWrapperTest < ActionController::TestCase
@@ -262,7 +276,7 @@ class NamespacedParamsWrapperTest < ActionController::TestCase
module Admin
module Users
- class UsersController < ActionController::Base;
+ class UsersController < ActionController::Base
class << self
attr_accessor :last_parameters
end
diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb
index e447b66486..2959dc3e4d 100644
--- a/actionpack/test/controller/redirect_test.rb
+++ b/actionpack/test/controller/redirect_test.rb
@@ -62,6 +62,10 @@ class RedirectController < ActionController::Base
redirect_back(fallback_location: "/things/stuff", status: 307)
end
+ def safe_redirect_back_with_status
+ redirect_back(fallback_location: "/things/stuff", status: 307, allow_other_host: false)
+ end
+
def host_redirect
redirect_to action: "other_host", only_path: false, host: "other.test.host"
end
@@ -259,6 +263,23 @@ class RedirectTest < ActionController::TestCase
assert_equal "http://test.host/things/stuff", redirect_to_url
end
+ def test_safe_redirect_back_from_other_host
+ @request.env["HTTP_REFERER"] = "http://another.host/coming/from"
+ get :safe_redirect_back_with_status
+
+ assert_response 307
+ assert_equal "http://test.host/things/stuff", redirect_to_url
+ end
+
+ def test_safe_redirect_back_from_the_same_host
+ referer = "http://test.host/coming/from"
+ @request.env["HTTP_REFERER"] = referer
+ get :safe_redirect_back_with_status
+
+ assert_response 307
+ assert_equal referer, redirect_to_url
+ end
+
def test_redirect_to_record
with_routing do |set|
set.draw do
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index 37a62edc15..24c5761e41 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -415,7 +415,7 @@ class LastModifiedRenderTest < ActionController::TestCase
@request.if_modified_since = @last_modified
get :conditional_hello
assert_equal 304, @response.status.to_i
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal @last_modified, @response.headers["Last-Modified"]
end
@@ -430,7 +430,7 @@ class LastModifiedRenderTest < ActionController::TestCase
@request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT"
get :conditional_hello
assert_equal 200, @response.status.to_i
- assert @response.body.present?
+ assert_predicate @response.body, :present?
assert_equal @last_modified, @response.headers["Last-Modified"]
end
@@ -443,7 +443,7 @@ class LastModifiedRenderTest < ActionController::TestCase
@request.if_modified_since = @last_modified
get :conditional_hello_with_record
assert_equal 304, @response.status.to_i
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_not_nil @response.etag
assert_equal @last_modified, @response.headers["Last-Modified"]
end
@@ -459,7 +459,7 @@ class LastModifiedRenderTest < ActionController::TestCase
@request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT"
get :conditional_hello_with_record
assert_equal 200, @response.status.to_i
- assert @response.body.present?
+ assert_predicate @response.body, :present?
assert_equal @last_modified, @response.headers["Last-Modified"]
end
@@ -472,7 +472,7 @@ class LastModifiedRenderTest < ActionController::TestCase
@request.if_modified_since = @last_modified
get :conditional_hello_with_collection_of_records
assert_equal 304, @response.status.to_i
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal @last_modified, @response.headers["Last-Modified"]
end
@@ -487,7 +487,7 @@ class LastModifiedRenderTest < ActionController::TestCase
@request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT"
get :conditional_hello_with_collection_of_records
assert_equal 200, @response.status.to_i
- assert @response.body.present?
+ assert_predicate @response.body, :present?
assert_equal @last_modified, @response.headers["Last-Modified"]
end
@@ -592,7 +592,7 @@ class EtagRenderTest < ActionController::TestCase
end
def strong_etag(record)
- %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}")
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}")
end
end
@@ -650,7 +650,7 @@ class ImplicitRenderTest < ActionController::TestCase
tests ImplicitRenderTestController
def test_implicit_no_content_response_as_browser
- assert_raises(ActionController::UnknownFormat) do
+ assert_raises(ActionController::MissingExactTemplate) do
get :empty_action
end
end
@@ -682,27 +682,27 @@ class HeadRenderTest < ActionController::TestCase
def test_head_created
post :head_created
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_response :created
end
def test_head_created_with_application_json_content_type
post :head_created_with_application_json_content_type
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal "application/json", @response.header["Content-Type"]
assert_response :created
end
def test_head_ok_with_image_png_content_type
post :head_ok_with_image_png_content_type
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal "image/png", @response.header["Content-Type"]
assert_response :ok
end
def test_head_with_location_header
get :head_with_location_header
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal "/foo", @response.headers["Location"]
assert_response :ok
end
@@ -718,7 +718,7 @@ class HeadRenderTest < ActionController::TestCase
end
get :head_with_location_object
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal "http://www.nextangle.com/customers/1", @response.headers["Location"]
assert_response :ok
end
@@ -726,14 +726,14 @@ class HeadRenderTest < ActionController::TestCase
def test_head_with_custom_header
get :head_with_custom_header
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal "something", @response.headers["X-Custom-Header"]
assert_response :ok
end
def test_head_with_www_authenticate_header
get :head_with_www_authenticate_header
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal "something", @response.headers["WWW-Authenticate"]
assert_response :ok
end
@@ -812,7 +812,7 @@ class HttpCacheForeverTest < ActionController::TestCase
assert_response :ok
assert_equal "max-age=#{100.years}, public", @response.headers["Cache-Control"]
assert_not_nil @response.etag
- assert @response.weak_etag?
+ assert_predicate @response, :weak_etag?
end
def test_cache_with_private
@@ -820,7 +820,7 @@ class HttpCacheForeverTest < ActionController::TestCase
assert_response :ok
assert_equal "max-age=#{100.years}, private", @response.headers["Cache-Control"]
assert_not_nil @response.etag
- assert @response.weak_etag?
+ assert_predicate @response, :weak_etag?
end
def test_cache_response_code_with_if_modified_since
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
index 12ae95d602..7a02c27c99 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -2,6 +2,7 @@
require "abstract_unit"
require "active_support/log_subscriber/test_helper"
+require "active_support/messages/rotation_configuration"
# common controller actions
module RequestForgeryProtectionActions
@@ -445,6 +446,19 @@ module RequestForgeryProtectionTests
end
end
+ def test_should_raise_for_post_with_null_origin
+ forgery_protection_origin_check do
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ exception = assert_raises(ActionController::InvalidAuthenticityToken) do
+ @request.set_header "HTTP_ORIGIN", "null"
+ post :index, params: { custom_authenticity_token: @token }
+ end
+ assert_match "The browser returned a 'null' origin for a request", exception.message
+ end
+ end
+ end
+
def test_should_block_post_with_origin_checking_and_wrong_origin
old_logger = ActionController::Base.logger
logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
@@ -630,13 +644,14 @@ end
class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
class NullSessionDummyKeyGenerator
- def generate_key(secret)
+ def generate_key(secret, length = nil)
"03312270731a2ed0d11ed091c2338a06"
end
end
def setup
@request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new
+ @request.env[ActionDispatch::Cookies::COOKIES_ROTATIONS] = ActiveSupport::Messages::RotationConfiguration.new
end
test "should allow to set signed cookies" do
@@ -731,7 +746,7 @@ class FreeCookieControllerTest < ActionController::TestCase
test "should not emit a csrf-token meta tag" do
SecureRandom.stub :base64, @token do
get :meta
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
end
end
end
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
index 3d98237003..30bea64c55 100644
--- a/actionpack/test/controller/resources_test.rb
+++ b/actionpack/test/controller/resources_test.rb
@@ -307,7 +307,7 @@ class ResourcesTest < ActionController::TestCase
set.draw do
resources :messages do
member do
- match :mark , via: method
+ match :mark, via: method
match :unmark, via: method
end
end
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
index f09051b306..259f3b8855 100644
--- a/actionpack/test/controller/routing_test.rb
+++ b/actionpack/test/controller/routing_test.rb
@@ -23,7 +23,7 @@ class UriReservedCharactersRoutingTest < ActiveSupport::TestCase
end
safe, unsafe = %w(: @ & = + $ , ;), %w(^ ? # [ ])
- hex = unsafe.map { |char| "%" + char.unpack("H2").first.upcase }
+ hex = unsafe.map { |char| "%" + char.unpack1("H2").upcase }
@segment = "#{safe.join}#{unsafe.join}".freeze
@escaped = "#{safe.join}#{hex.join}".freeze
@@ -213,7 +213,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
assert_equal expected, ActiveSupport::JSON.decode(get(u))
end
- def test_regexp_precidence
+ def test_regexp_precedence
rs.draw do
get "/whois/:domain", constraints: {
domain: /\w+\.[\w\.]+/ },
@@ -676,7 +676,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
token = "\321\202\320\265\320\272\321\201\321\202".dup # 'text' in Russian
token.force_encoding(Encoding::BINARY)
- escaped_token = CGI::escape(token)
+ escaped_token = CGI.escape(token)
assert_equal "/page/" + escaped_token, url_for(rs, controller: "content", action: "show_page", id: token)
assert_equal({ controller: "content", action: "show_page", id: token }, rs.recognize_path("/page/#{escaped_token}"))
@@ -1687,7 +1687,7 @@ class RouteSetTest < ActiveSupport::TestCase
def test_routes_with_symbols
set.draw do
get "unnamed", controller: :pages, action: :show, name: :as_symbol
- get "named" , controller: :pages, action: :show, name: :as_symbol, as: :named
+ get "named", controller: :pages, action: :show, name: :as_symbol, as: :named
end
assert_equal({ controller: "pages", action: "show", name: :as_symbol }, set.recognize_path("/unnamed"))
assert_equal({ controller: "pages", action: "show", name: :as_symbol }, set.recognize_path("/named"))
@@ -1893,7 +1893,7 @@ class RouteSetTest < ActiveSupport::TestCase
assert_equal({ controller: "blog", action: "show_date", year: "2006", month: "07", day: "28" }, controller.request.path_parameters)
assert_equal("/blog/2006/07/25", controller.url_for(day: 25, only_path: true))
assert_equal("/blog/2005", controller.url_for(year: 2005, only_path: true))
- assert_equal("/blog/show/123", controller.url_for(action: "show" , id: 123, only_path: true))
+ assert_equal("/blog/show/123", controller.url_for(action: "show", id: 123, only_path: true))
assert_equal("/blog/2006", controller.url_for(year: 2006, only_path: true))
assert_equal("/blog/2006", controller.url_for(year: 2006, month: nil, only_path: true))
end
diff --git a/actionpack/test/controller/runner_test.rb b/actionpack/test/controller/runner_test.rb
index a96c9c519b..1709ab5f6d 100644
--- a/actionpack/test/controller/runner_test.rb
+++ b/actionpack/test/controller/runner_test.rb
@@ -17,8 +17,8 @@ module ActionDispatch
def test_respond_to?
runner = MyRunner.new(Class.new { def x; end }.new)
- assert runner.respond_to?(:hi)
- assert runner.respond_to?(:x)
+ assert_respond_to runner, :hi
+ assert_respond_to runner, :x
end
end
end
diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb
index fd2399e433..7b1a52b277 100644
--- a/actionpack/test/controller/send_file_test.rb
+++ b/actionpack/test/controller/send_file_test.rb
@@ -178,7 +178,7 @@ class SendFileTest < ActionController::TestCase
"image.jpg" => "image/jpeg",
"image.tif" => "image/tiff",
"image.gif" => "image/gif",
- "movie.mpg" => "video/mpeg",
+ "movie.mp4" => "video/mp4",
"file.zip" => "application/zip",
"file.unk" => "application/octet-stream",
"zip" => "application/octet-stream"
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index 46369ebbb0..e66c409786 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -6,7 +6,7 @@ require "active_support/json/decoding"
require "rails/engine"
class TestCaseTest < ActionController::TestCase
- def self.fixture_path; end;
+ def self.fixture_path; end
class TestController < ActionController::Base
def no_op
@@ -670,7 +670,7 @@ XML
assert_equal "bar", @request.params[:foo]
post :no_op
- assert @request.params[:foo].blank?
+ assert_predicate @request.params[:foo], :blank?
end
def test_filtered_parameters_reset_between_requests
@@ -681,6 +681,14 @@ XML
assert_equal "baz", @request.filtered_parameters[:foo]
end
+ def test_raw_post_reset_between_post_requests
+ post :no_op, params: { foo: "bar" }
+ assert_equal "foo=bar", @request.raw_post
+
+ post :no_op, params: { foo: "baz" }
+ assert_equal "foo=baz", @request.raw_post
+ end
+
def test_path_is_kept_after_the_request
get :test_params, params: { id: "foo" }
assert_equal "/test_case_test/test/test_params/foo", @request.path
@@ -740,6 +748,14 @@ XML
assert_equal "application/json", @response.body
end
+ def test_request_format_kwarg_doesnt_mutate_params
+ params = { foo: "bar" }.freeze
+
+ assert_nothing_raised do
+ get :test_format, format: "json", params: params
+ end
+ end
+
def test_should_have_knowledge_of_client_side_cookie_state_even_if_they_are_not_set
cookies["foo"] = "bar"
get :no_op
@@ -838,7 +854,7 @@ XML
def test_fixture_file_upload_should_be_able_access_to_tempfile
file = fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpg")
- assert file.respond_to?(:tempfile), "expected tempfile should respond on fixture file object, got nothing"
+ assert_respond_to file, :tempfile
end
def test_fixture_file_upload
diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb
index a7c7356921..a1521da702 100644
--- a/actionpack/test/controller/url_for_integration_test.rb
+++ b/actionpack/test/controller/url_for_integration_test.rb
@@ -35,7 +35,6 @@ module ActionPack
as: "blog"
resources :people
- #match 'legacy/people' => "people#index", :legacy => "true"
get "symbols", controller: :symbols, action: :show, name: :as_symbol
get "id_default(/:id)" => "foo#id_default", :id => 1
diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb
index cf11227897..e381abee36 100644
--- a/actionpack/test/controller/url_for_test.rb
+++ b/actionpack/test/controller/url_for_test.rb
@@ -288,7 +288,7 @@ module AbstractController
kls = Class.new { include set.url_helpers }
controller = kls.new
- assert controller.respond_to?(:home_url)
+ assert_respond_to controller, :home_url
assert_equal "http://www.basecamphq.com/home/sweet/home/again",
controller.send(:home_url, host: "www.basecamphq.com", user: "again")
diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb
index 0f79c83b6d..ca83b850d5 100644
--- a/actionpack/test/controller/url_rewriter_test.rb
+++ b/actionpack/test/controller/url_rewriter_test.rb
@@ -19,7 +19,7 @@ class UrlRewriterTests < ActionController::TestCase
def setup
@params = {}
- @rewriter = Rewriter.new(@request) #.new(@request, @params)
+ @rewriter = Rewriter.new(@request)
@routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
r.draw do
ActiveSupport::Deprecation.silence do
diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb
new file mode 100644
index 0000000000..c4c7f53903
--- /dev/null
+++ b/actionpack/test/dispatch/content_security_policy_test.rb
@@ -0,0 +1,519 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ContentSecurityPolicyTest < ActiveSupport::TestCase
+ def setup
+ @policy = ActionDispatch::ContentSecurityPolicy.new
+ end
+
+ def test_build
+ assert_equal "", @policy.build
+
+ @policy.script_src :self
+ assert_equal "script-src 'self'", @policy.build
+ end
+
+ def test_dup
+ @policy.img_src :self
+ @policy.block_all_mixed_content
+ @policy.upgrade_insecure_requests
+ @policy.sandbox
+ copied = @policy.dup
+ assert_equal copied.build, @policy.build
+ end
+
+ def test_mappings
+ @policy.script_src :data
+ assert_equal "script-src data:", @policy.build
+
+ @policy.script_src :mediastream
+ assert_equal "script-src mediastream:", @policy.build
+
+ @policy.script_src :blob
+ assert_equal "script-src blob:", @policy.build
+
+ @policy.script_src :filesystem
+ assert_equal "script-src filesystem:", @policy.build
+
+ @policy.script_src :self
+ assert_equal "script-src 'self'", @policy.build
+
+ @policy.script_src :unsafe_inline
+ assert_equal "script-src 'unsafe-inline'", @policy.build
+
+ @policy.script_src :unsafe_eval
+ assert_equal "script-src 'unsafe-eval'", @policy.build
+
+ @policy.script_src :none
+ assert_equal "script-src 'none'", @policy.build
+
+ @policy.script_src :strict_dynamic
+ assert_equal "script-src 'strict-dynamic'", @policy.build
+
+ @policy.script_src :ws
+ assert_equal "script-src ws:", @policy.build
+
+ @policy.script_src :wss
+ assert_equal "script-src wss:", @policy.build
+
+ @policy.script_src :none, :report_sample
+ assert_equal "script-src 'none' 'report-sample'", @policy.build
+ end
+
+ def test_fetch_directives
+ @policy.child_src :self
+ assert_match %r{child-src 'self'}, @policy.build
+
+ @policy.child_src false
+ assert_no_match %r{child-src}, @policy.build
+
+ @policy.connect_src :self
+ assert_match %r{connect-src 'self'}, @policy.build
+
+ @policy.connect_src false
+ assert_no_match %r{connect-src}, @policy.build
+
+ @policy.default_src :self
+ assert_match %r{default-src 'self'}, @policy.build
+
+ @policy.default_src false
+ assert_no_match %r{default-src}, @policy.build
+
+ @policy.font_src :self
+ assert_match %r{font-src 'self'}, @policy.build
+
+ @policy.font_src false
+ assert_no_match %r{font-src}, @policy.build
+
+ @policy.frame_src :self
+ assert_match %r{frame-src 'self'}, @policy.build
+
+ @policy.frame_src false
+ assert_no_match %r{frame-src}, @policy.build
+
+ @policy.img_src :self
+ assert_match %r{img-src 'self'}, @policy.build
+
+ @policy.img_src false
+ assert_no_match %r{img-src}, @policy.build
+
+ @policy.manifest_src :self
+ assert_match %r{manifest-src 'self'}, @policy.build
+
+ @policy.manifest_src false
+ assert_no_match %r{manifest-src}, @policy.build
+
+ @policy.media_src :self
+ assert_match %r{media-src 'self'}, @policy.build
+
+ @policy.media_src false
+ assert_no_match %r{media-src}, @policy.build
+
+ @policy.object_src :self
+ assert_match %r{object-src 'self'}, @policy.build
+
+ @policy.object_src false
+ assert_no_match %r{object-src}, @policy.build
+
+ @policy.script_src :self
+ assert_match %r{script-src 'self'}, @policy.build
+
+ @policy.script_src false
+ assert_no_match %r{script-src}, @policy.build
+
+ @policy.style_src :self
+ assert_match %r{style-src 'self'}, @policy.build
+
+ @policy.style_src false
+ assert_no_match %r{style-src}, @policy.build
+
+ @policy.worker_src :self
+ assert_match %r{worker-src 'self'}, @policy.build
+
+ @policy.worker_src false
+ assert_no_match %r{worker-src}, @policy.build
+ end
+
+ def test_document_directives
+ @policy.base_uri "https://example.com"
+ assert_match %r{base-uri https://example\.com}, @policy.build
+
+ @policy.plugin_types "application/x-shockwave-flash"
+ assert_match %r{plugin-types application/x-shockwave-flash}, @policy.build
+
+ @policy.sandbox
+ assert_match %r{sandbox}, @policy.build
+
+ @policy.sandbox "allow-scripts", "allow-modals"
+ assert_match %r{sandbox allow-scripts allow-modals}, @policy.build
+
+ @policy.sandbox false
+ assert_no_match %r{sandbox}, @policy.build
+ end
+
+ def test_navigation_directives
+ @policy.form_action :self
+ assert_match %r{form-action 'self'}, @policy.build
+
+ @policy.frame_ancestors :self
+ assert_match %r{frame-ancestors 'self'}, @policy.build
+ end
+
+ def test_reporting_directives
+ @policy.report_uri "/violations"
+ assert_match %r{report-uri /violations}, @policy.build
+ end
+
+ def test_other_directives
+ @policy.block_all_mixed_content
+ assert_match %r{block-all-mixed-content}, @policy.build
+
+ @policy.block_all_mixed_content false
+ assert_no_match %r{block-all-mixed-content}, @policy.build
+
+ @policy.require_sri_for :script, :style
+ assert_match %r{require-sri-for script style}, @policy.build
+
+ @policy.require_sri_for "script", "style"
+ assert_match %r{require-sri-for script style}, @policy.build
+
+ @policy.require_sri_for
+ assert_no_match %r{require-sri-for}, @policy.build
+
+ @policy.upgrade_insecure_requests
+ assert_match %r{upgrade-insecure-requests}, @policy.build
+
+ @policy.upgrade_insecure_requests false
+ assert_no_match %r{upgrade-insecure-requests}, @policy.build
+ end
+
+ def test_multiple_sources
+ @policy.script_src :self, :https
+ assert_equal "script-src 'self' https:", @policy.build
+ end
+
+ def test_multiple_directives
+ @policy.script_src :self, :https
+ @policy.style_src :self, :https
+ assert_equal "script-src 'self' https:; style-src 'self' https:", @policy.build
+ end
+
+ def test_dynamic_directives
+ request = ActionDispatch::Request.new("HTTP_HOST" => "www.example.com")
+ controller = Struct.new(:request).new(request)
+
+ @policy.script_src -> { request.host }
+ assert_equal "script-src www.example.com", @policy.build(controller)
+ end
+
+ def test_mixed_static_and_dynamic_directives
+ @policy.script_src :self, -> { "foo.com" }, "bar.com"
+ request = ActionDispatch::Request.new({})
+ controller = Struct.new(:request).new(request)
+ assert_equal "script-src 'self' foo.com bar.com", @policy.build(controller)
+ end
+
+ def test_invalid_directive_source
+ exception = assert_raises(ArgumentError) do
+ @policy.script_src [:self]
+ end
+
+ assert_equal "Invalid content security policy source: [:self]", exception.message
+ end
+
+ def test_missing_context_for_dynamic_source
+ @policy.script_src -> { request.host }
+
+ exception = assert_raises(RuntimeError) do
+ @policy.build
+ end
+
+ assert_match %r{\AMissing context for the dynamic content security policy source:}, exception.message
+ end
+
+ def test_raises_runtime_error_when_unexpected_source
+ @policy.plugin_types [:flash]
+
+ exception = assert_raises(RuntimeError) do
+ @policy.build
+ end
+
+ assert_match %r{\AUnexpected content security policy source:}, exception.message
+ end
+end
+
+class DefaultContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
+ class PolicyController < ActionController::Base
+ def index
+ head :ok
+ end
+ end
+
+ ROUTES = ActionDispatch::Routing::RouteSet.new
+ ROUTES.draw do
+ scope module: "default_content_security_policy_integration_test" do
+ get "/", to: "policy#index"
+ end
+ end
+
+ POLICY = ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src :self
+ p.script_src :https
+ end
+
+ class PolicyConfigMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ env["action_dispatch.content_security_policy"] = POLICY
+ env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" }
+ env["action_dispatch.content_security_policy_report_only"] = false
+ env["action_dispatch.show_exceptions"] = false
+
+ @app.call(env)
+ end
+ end
+
+ APP = build_app(ROUTES) do |middleware|
+ middleware.use PolicyConfigMiddleware
+ middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
+ end
+
+ def app
+ APP
+ end
+
+ def test_adds_nonce_to_script_src_content_security_policy_only_once
+ get "/"
+ get "/"
+ assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='"
+ end
+
+ private
+
+ def assert_policy(expected, report_only: false)
+ assert_response :success
+
+ if report_only
+ expected_header = "Content-Security-Policy-Report-Only"
+ unexpected_header = "Content-Security-Policy"
+ else
+ expected_header = "Content-Security-Policy"
+ unexpected_header = "Content-Security-Policy-Report-Only"
+ end
+
+ assert_nil response.headers[unexpected_header]
+ assert_equal expected, response.headers[expected_header]
+ end
+end
+
+class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
+ class PolicyController < ActionController::Base
+ content_security_policy only: :inline do |p|
+ p.default_src "https://example.com"
+ end
+
+ content_security_policy only: :conditional, if: :condition? do |p|
+ p.default_src "https://true.example.com"
+ end
+
+ content_security_policy only: :conditional, unless: :condition? do |p|
+ p.default_src "https://false.example.com"
+ end
+
+ content_security_policy only: :report_only do |p|
+ p.report_uri "/violations"
+ end
+
+ content_security_policy only: :script_src do |p|
+ p.default_src false
+ p.script_src :self
+ end
+
+ content_security_policy(false, only: :no_policy)
+
+ content_security_policy_report_only only: :report_only
+
+ def index
+ head :ok
+ end
+
+ def inline
+ head :ok
+ end
+
+ def conditional
+ head :ok
+ end
+
+ def report_only
+ head :ok
+ end
+
+ def script_src
+ head :ok
+ end
+
+ def no_policy
+ head :ok
+ end
+
+ private
+ def condition?
+ params[:condition] == "true"
+ end
+ end
+
+ ROUTES = ActionDispatch::Routing::RouteSet.new
+ ROUTES.draw do
+ scope module: "content_security_policy_integration_test" do
+ get "/", to: "policy#index"
+ get "/inline", to: "policy#inline"
+ get "/conditional", to: "policy#conditional"
+ get "/report-only", to: "policy#report_only"
+ get "/script-src", to: "policy#script_src"
+ get "/no-policy", to: "policy#no_policy"
+ end
+ end
+
+ POLICY = ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src :self
+ end
+
+ class PolicyConfigMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ env["action_dispatch.content_security_policy"] = POLICY
+ env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" }
+ env["action_dispatch.content_security_policy_report_only"] = false
+ env["action_dispatch.show_exceptions"] = false
+
+ @app.call(env)
+ end
+ end
+
+ APP = build_app(ROUTES) do |middleware|
+ middleware.use PolicyConfigMiddleware
+ middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
+ end
+
+ def app
+ APP
+ end
+
+ def test_generates_content_security_policy_header
+ get "/"
+ assert_policy "default-src 'self'"
+ end
+
+ def test_generates_inline_content_security_policy
+ get "/inline"
+ assert_policy "default-src https://example.com"
+ end
+
+ def test_generates_conditional_content_security_policy
+ get "/conditional", params: { condition: "true" }
+ assert_policy "default-src https://true.example.com"
+
+ get "/conditional", params: { condition: "false" }
+ assert_policy "default-src https://false.example.com"
+ end
+
+ def test_generates_report_only_content_security_policy
+ get "/report-only"
+ assert_policy "default-src 'self'; report-uri /violations", report_only: true
+ end
+
+ def test_adds_nonce_to_script_src_content_security_policy
+ get "/script-src"
+ assert_policy "script-src 'self' 'nonce-iyhD0Yc0W+c='"
+ end
+
+ def test_generates_no_content_security_policy
+ get "/no-policy"
+
+ assert_nil response.headers["Content-Security-Policy"]
+ assert_nil response.headers["Content-Security-Policy-Report-Only"]
+ end
+
+ private
+
+ def assert_policy(expected, report_only: false)
+ assert_response :success
+
+ if report_only
+ expected_header = "Content-Security-Policy-Report-Only"
+ unexpected_header = "Content-Security-Policy"
+ else
+ expected_header = "Content-Security-Policy"
+ unexpected_header = "Content-Security-Policy-Report-Only"
+ end
+
+ assert_nil response.headers[unexpected_header]
+ assert_equal expected, response.headers[expected_header]
+ end
+end
+
+class DisabledContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
+ class PolicyController < ActionController::Base
+ content_security_policy only: :inline do |p|
+ p.default_src "https://example.com"
+ end
+
+ def index
+ head :ok
+ end
+
+ def inline
+ head :ok
+ end
+ end
+
+ ROUTES = ActionDispatch::Routing::RouteSet.new
+ ROUTES.draw do
+ scope module: "disabled_content_security_policy_integration_test" do
+ get "/", to: "policy#index"
+ get "/inline", to: "policy#inline"
+ end
+ end
+
+ class PolicyConfigMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ env["action_dispatch.content_security_policy"] = nil
+ env["action_dispatch.content_security_policy_nonce_generator"] = nil
+ env["action_dispatch.content_security_policy_report_only"] = false
+ env["action_dispatch.show_exceptions"] = false
+
+ @app.call(env)
+ end
+ end
+
+ APP = build_app(ROUTES) do |middleware|
+ middleware.use PolicyConfigMiddleware
+ middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
+ end
+
+ def app
+ APP
+ end
+
+ def test_generates_no_content_security_policy_by_default
+ get "/"
+ assert_nil response.headers["Content-Security-Policy"]
+ end
+
+ def test_generates_content_security_policy_header_when_globally_disabled
+ get "/inline"
+ assert_equal "default-src https://example.com", response.headers["Content-Security-Policy"]
+ end
+end
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index cb225c0f62..aba778fad6 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -3,7 +3,7 @@
require "abstract_unit"
require "openssl"
require "active_support/key_generator"
-require "active_support/message_verifier"
+require "active_support/messages/rotation_configuration"
class CookieJarTest < ActiveSupport::TestCase
attr_reader :request
@@ -36,6 +36,12 @@ class CookieJarTest < ActiveSupport::TestCase
assert_equal "bar", request.cookie_jar.fetch(:foo)
end
+ def test_to_hash
+ request.cookie_jar["foo"] = "bar"
+ assert_equal({ "foo" => "bar" }, request.cookie_jar.to_hash)
+ assert_equal({ "foo" => "bar" }, request.cookie_jar.to_h)
+ end
+
def test_fetch_type_error
assert_raises(KeyError) do
request.cookie_jar.fetch(:omglolwut)
@@ -59,8 +65,8 @@ class CookieJarTest < ActiveSupport::TestCase
end
def test_key_methods
- assert !request.cookie_jar.key?(:foo)
- assert !request.cookie_jar.has_key?("foo")
+ assert_not request.cookie_jar.key?(:foo)
+ assert_not request.cookie_jar.has_key?("foo")
request.cookie_jar[:foo] = :bar
assert request.cookie_jar.key?(:foo)
@@ -287,15 +293,25 @@ class CookiesTest < ActionController::TestCase
tests TestController
- SALT = "b3c631c314c0bbca50c1b2843150fe33"
+ SECRET_KEY_BASE = "b3c631c314c0bbca50c1b2843150fe33"
+ SIGNED_COOKIE_SALT = "signed cookie"
+ ENCRYPTED_COOKIE_SALT = "encrypted cookie"
+ ENCRYPTED_SIGNED_COOKIE_SALT = "sigend encrypted cookie"
+ AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "authenticated encrypted cookie"
def setup
super
- @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SALT, iterations: 2)
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SECRET_KEY_BASE, iterations: 2)
+ @request.env["action_dispatch.cookies_rotations"] = ActiveSupport::Messages::RotationConfiguration.new
- @request.env["action_dispatch.signed_cookie_salt"] =
- @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] = SALT
+ @request.env["action_dispatch.secret_key_base"] = SECRET_KEY_BASE
+ @request.env["action_dispatch.use_authenticated_cookie_encryption"] = true
+
+ @request.env["action_dispatch.signed_cookie_salt"] = SIGNED_COOKIE_SALT
+ @request.env["action_dispatch.encrypted_cookie_salt"] = ENCRYPTED_COOKIE_SALT
+ @request.env["action_dispatch.encrypted_signed_cookie_salt"] = ENCRYPTED_SIGNED_COOKIE_SALT
+ @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] = AUTHENTICATED_ENCRYPTED_COOKIE_SALT
@request.host = "www.nextangle.com"
end
@@ -309,7 +325,7 @@ class CookiesTest < ActionController::TestCase
def test_setting_the_same_value_to_cookie
request.cookies[:user_name] = "david"
get :authenticate
- assert_predicate response.cookies, :empty?
+ assert_empty response.cookies
end
def test_setting_the_same_value_to_permanent_cookie
@@ -391,7 +407,7 @@ class CookiesTest < ActionController::TestCase
def test_delete_unexisting_cookie
request.cookies.clear
get :delete_cookie
- assert_predicate @response.cookies, :empty?
+ assert_empty @response.cookies
end
def test_deleted_cookie_predicate
@@ -430,28 +446,72 @@ class CookiesTest < ActionController::TestCase
assert_equal 45, cookies.signed[:user_id]
key_generator = @request.env["action_dispatch.key_generator"]
- signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
- secret = key_generator.generate_key(signed_cookie_salt)
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA1")
assert_equal verifier.generate(45), cookies[:user_id]
end
def test_signed_cookie_using_custom_digest
- @request.env["action_dispatch.cookies_digest"] = "SHA256"
+ @request.env["action_dispatch.signed_cookie_digest"] = "SHA256"
+
get :set_signed_cookie
cookies = @controller.send :cookies
assert_not_equal 45, cookies[:user_id]
assert_equal 45, cookies.signed[:user_id]
key_generator = @request.env["action_dispatch.key_generator"]
- signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
- secret = key_generator.generate_key(signed_cookie_salt)
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA256")
assert_equal verifier.generate(45), cookies[:user_id]
end
+ def test_signed_cookie_rotating_secret_and_digest
+ secret = "b3c631c314c0bbca50c1b2843150fe33"
+
+ @request.env["action_dispatch.signed_cookie_digest"] = "SHA256"
+ @request.env["action_dispatch.cookies_rotations"].rotate :signed, secret, digest: "SHA1"
+
+ old_message = ActiveSupport::MessageVerifier.new(secret, digest: "SHA1", serializer: Marshal).generate(45)
+ @request.headers["Cookie"] = "user_id=#{old_message}"
+
+ get :get_signed_cookie
+ assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+ verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA256", serializer: Marshal)
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ end
+
+ def test_signed_cookie_with_legacy_secret_scheme
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+
+ old_message = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", digest: "SHA1", serializer: Marshal).generate(45)
+
+ @request.headers["Cookie"] = "user_id=#{old_message}"
+ get :get_signed_cookie
+ assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key("signed cookie")
+ verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA1", serializer: Marshal)
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ end
+
+ def test_tampered_with_signed_cookie
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA1")
+ message = verifier.generate(45)
+
+ @request.headers["Cookie"] = "user_id=#{Marshal.dump 45}--#{message.split("--").last}"
+ get :get_signed_cookie
+ assert_nil @controller.send(:cookies).signed[:user_id]
+ end
+
def test_signed_cookie_using_default_serializer
get :set_signed_cookie
cookies = @controller.send :cookies
@@ -494,8 +554,7 @@ class CookiesTest < ActionController::TestCase
@request.env["action_dispatch.cookies_serializer"] = :hybrid
key_generator = @request.env["action_dispatch.key_generator"]
- signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
- secret = key_generator.generate_key(signed_cookie_salt)
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
marshal_value = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal).generate(45)
@request.headers["Cookie"] = "user_id=#{marshal_value}"
@@ -514,8 +573,8 @@ class CookiesTest < ActionController::TestCase
@request.env["action_dispatch.cookies_serializer"] = :hybrid
key_generator = @request.env["action_dispatch.key_generator"]
- signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
- secret = key_generator.generate_key(signed_cookie_salt)
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+
json_value = ActiveSupport::MessageVerifier.new(secret, serializer: JSON).generate(45)
@request.headers["Cookie"] = "user_id=#{json_value}"
@@ -578,11 +637,10 @@ class CookiesTest < ActionController::TestCase
def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json
@request.env["action_dispatch.cookies_serializer"] = :hybrid
- cipher = "aes-256-gcm"
- salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
- secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
- encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal)
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32)
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal)
marshal_value = encryptor.encrypt_and_sign("bar")
@request.headers["Cookie"] = "foo=#{::Rack::Utils.escape marshal_value}"
@@ -592,7 +650,7 @@ class CookiesTest < ActionController::TestCase
assert_not_equal "bar", cookies[:foo]
assert_equal "bar", cookies.encrypted[:foo]
- json_encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
+ json_encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON)
assert_not_nil @response.cookies["foo"]
assert_equal "bar", json_encryptor.decrypt_and_verify(@response.cookies["foo"])
end
@@ -600,11 +658,10 @@ class CookiesTest < ActionController::TestCase
def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value
@request.env["action_dispatch.cookies_serializer"] = :hybrid
- cipher = "aes-256-gcm"
- salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
- secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
- encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32)
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON)
json_value = encryptor.encrypt_and_sign("bar")
@request.headers["Cookie"] = "foo=#{::Rack::Utils.escape json_value}"
@@ -691,65 +748,8 @@ class CookiesTest < ActionController::TestCase
}
end
- def test_signed_uses_signed_cookie_jar_if_only_secret_token_is_set
- @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = nil
- get :set_signed_cookie
- assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed
- end
-
- def test_signed_uses_signed_cookie_jar_if_only_secret_key_base_is_set
- @request.env["action_dispatch.secret_token"] = nil
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- get :set_signed_cookie
- assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed
- end
-
- def test_signed_uses_upgrade_legacy_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
- @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- get :set_signed_cookie
- assert_kind_of ActionDispatch::Cookies::UpgradeLegacySignedCookieJar, cookies.signed
- end
-
- def test_signed_or_encrypted_uses_signed_cookie_jar_if_only_secret_token_is_set
- @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = nil
- get :get_encrypted_cookie
- assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed_or_encrypted
- end
-
- def test_signed_or_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set
- @request.env["action_dispatch.secret_token"] = nil
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- get :get_encrypted_cookie
- assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.signed_or_encrypted
- end
-
- def test_signed_or_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
- @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- get :get_encrypted_cookie
- assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.signed_or_encrypted
- end
-
- def test_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set
- @request.env["action_dispatch.secret_token"] = nil
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- get :get_encrypted_cookie
- assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.encrypted
- end
-
- def test_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
- @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- get :get_encrypted_cookie
- assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.encrypted
- end
-
def test_legacy_signed_cookie_is_read_and_transparently_upgraded_by_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45)
@@ -766,9 +766,6 @@ class CookiesTest < ActionController::TestCase
def test_legacy_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
- @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate("bar")
@@ -777,17 +774,14 @@ class CookiesTest < ActionController::TestCase
assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
- cipher = "aes-256-gcm"
- salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
- secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
- encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal)
+ secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32)
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal)
assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.cookies_serializer"] = :json
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45)
@@ -805,7 +799,6 @@ class CookiesTest < ActionController::TestCase
def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.cookies_serializer"] = :json
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar")
@@ -824,7 +817,6 @@ class CookiesTest < ActionController::TestCase
def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.cookies_serializer"] = :hybrid
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45)
@@ -842,7 +834,6 @@ class CookiesTest < ActionController::TestCase
def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.cookies_serializer"] = :hybrid
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar")
@@ -851,17 +842,15 @@ class CookiesTest < ActionController::TestCase
assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
- cipher = "aes-256-gcm"
salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
- secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
- encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
+ secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")]
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON)
assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_legacy_marshal_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.cookies_serializer"] = :hybrid
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45)
@@ -878,6 +867,8 @@ class CookiesTest < ActionController::TestCase
def test_legacy_marshal_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.cookies_serializer"] = :hybrid
+
+ @request.env["action_dispatch.use_authenticated_cookie_encryption"] = true
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
@@ -888,16 +879,14 @@ class CookiesTest < ActionController::TestCase
assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
- cipher = "aes-256-gcm"
salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
- secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
- encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
+ secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")]
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON)
assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_legacy_signed_cookie_is_treated_as_nil_by_signed_cookie_jar_if_tampered
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
@request.headers["Cookie"] = "user_id=45"
get :get_signed_cookie
@@ -908,7 +897,6 @@ class CookiesTest < ActionController::TestCase
def test_legacy_signed_cookie_is_treated_as_nil_by_encrypted_cookie_jar_if_tampered
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
@request.headers["Cookie"] = "foo=baz"
get :get_encrypted_cookie
@@ -917,18 +905,50 @@ class CookiesTest < ActionController::TestCase
assert_nil @response.cookies["foo"]
end
- def test_legacy_hmac_aes_cbc_encrypted_marshal_cookie_is_upgraded_to_authenticated_encrypted_cookie
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ def test_use_authenticated_cookie_encryption_uses_legacy_hmac_aes_cbc_encryption_when_not_enabled
+ @request.env["action_dispatch.use_authenticated_cookie_encryption"] = nil
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
+ encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
+ secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc"))
+ sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", digest: "SHA1", serializer: Marshal)
+
+ get :set_encrypted_cookie
+
+ cookies = @controller.send :cookies
+ assert_not_equal "bar", cookies[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
+ assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
- @request.env["action_dispatch.encrypted_cookie_salt"] =
- @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT
+ def test_rotating_signed_cookies_digest
+ @request.env["action_dispatch.signed_cookie_digest"] = "SHA256"
+ @request.env["action_dispatch.cookies_rotations"].rotate :signed, digest: "SHA1"
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+
+ old_secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+ old_value = ActiveSupport::MessageVerifier.new(old_secret).generate(45)
+
+ @request.headers["Cookie"] = "user_id=#{old_value}"
+ get :get_signed_cookie
+
+ assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+ verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA256")
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ end
+ def test_legacy_hmac_aes_cbc_encrypted_marshal_cookie_is_upgraded_to_authenticated_encrypted_cookie
key_generator = @request.env["action_dispatch.key_generator"]
encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
- secret = key_generator.generate_key(encrypted_cookie_salt)
+ secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc"))
sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
- marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: Marshal).encrypt_and_sign("bar")
+ marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: Marshal).encrypt_and_sign("bar")
@request.headers["Cookie"] = "foo=#{marshal_value}"
@@ -938,27 +958,22 @@ class CookiesTest < ActionController::TestCase
assert_not_equal "bar", cookies[:foo]
assert_equal "bar", cookies.encrypted[:foo]
- aead_cipher = "aes-256-gcm"
aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
- aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)]
- aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: Marshal)
+ aead_secret = key_generator.generate_key(aead_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm"))
+ aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: "aes-256-gcm", serializer: Marshal)
assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_legacy_hmac_aes_cbc_encrypted_json_cookie_is_upgraded_to_authenticated_encrypted_cookie
@request.env["action_dispatch.cookies_serializer"] = :json
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
-
- @request.env["action_dispatch.encrypted_cookie_salt"] =
- @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT
key_generator = @request.env["action_dispatch.key_generator"]
encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
- secret = key_generator.generate_key(encrypted_cookie_salt)
+ secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc"))
sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
- marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON).encrypt_and_sign("bar")
+ marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: JSON).encrypt_and_sign("bar")
@request.headers["Cookie"] = "foo=#{marshal_value}"
@@ -968,19 +983,17 @@ class CookiesTest < ActionController::TestCase
assert_not_equal "bar", cookies[:foo]
assert_equal "bar", cookies.encrypted[:foo]
- aead_cipher = "aes-256-gcm"
aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
- aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)]
- aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: JSON)
+ aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")]
+ aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: "aes-256-gcm", serializer: JSON)
assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_legacy_hmac_aes_cbc_encrypted_cookie_using_64_byte_key_is_upgraded_to_authenticated_encrypted_cookie
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
-
- @request.env["action_dispatch.encrypted_cookie_salt"] =
- @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT
+ @request.env["action_dispatch.encrypted_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
# Cookie generated with 64 bytes secret
message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*")
@@ -991,15 +1004,35 @@ class CookiesTest < ActionController::TestCase
cookies = @controller.send :cookies
assert_not_equal "bar", cookies[:foo]
assert_equal "bar", cookies.encrypted[:foo]
- cipher = "aes-256-gcm"
salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
- secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
- encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal)
+ secret = @request.env["action_dispatch.key_generator"].generate_key(salt, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm"))
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal)
assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
+ def test_encrypted_cookie_rotating_secret
+ secret = "b3c631c314c0bbca50c1b2843150fe33"
+
+ @request.env["action_dispatch.encrypted_cookie_cipher"] = "aes-256-gcm"
+ @request.env["action_dispatch.cookies_rotations"].rotate :encrypted, secret
+
+ key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")
+
+ old_message = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal).encrypt_and_sign(45)
+
+ @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape old_message}"
+
+ get :get_encrypted_cookie
+ assert_equal 45, @controller.send(:cookies).encrypted[:foo]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], key_len)
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal)
+ assert_equal 45, encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
+
def test_cookie_with_all_domain_option
get :set_cookie_with_domain
assert_response :success
diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb
index 60acba0616..045567ff83 100644
--- a/actionpack/test/dispatch/debug_exceptions_test.rb
+++ b/actionpack/test/dispatch/debug_exceptions_test.rb
@@ -3,6 +3,8 @@
require "abstract_unit"
class DebugExceptionsTest < ActionDispatch::IntegrationTest
+ InterceptedErrorInstance = StandardError.new
+
class Boomer
attr_accessor :closed
@@ -36,6 +38,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
raise RuntimeError
when %r{/method_not_allowed}
raise ActionController::MethodNotAllowed
+ when %r{/intercepted_error}
+ raise InterceptedErrorInstance
when %r{/unknown_http_method}
raise ActionController::UnknownHttpMethod
when %r{/not_implemented}
@@ -76,9 +80,13 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
end
end
+ Interceptor = proc { |request, exception| request.set_header("int", exception) }
+ BadInterceptor = proc { |request, exception| raise "bad" }
RoutesApp = Struct.new(:routes).new(SharedTestRoutes)
ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false), RoutesApp)
DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp)
+ InterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [Interceptor])
+ BadInterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [BadInterceptor])
test "skip diagnosis if not showing detailed exceptions" do
@app = ProductionApp
@@ -499,4 +507,20 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
end
end
end
+
+ test "invoke interceptors before rendering" do
+ @app = InterceptedApp
+ get "/intercepted_error", headers: { "action_dispatch.show_exceptions" => true }
+
+ assert_equal InterceptedErrorInstance, request.get_header("int")
+ end
+
+ test "bad interceptors doesn't debug exceptions" do
+ @app = BadInterceptedApp
+
+ get "/puke", headers: { "action_dispatch.show_exceptions" => true }
+
+ assert_response 500
+ assert_match(/puke/, body)
+ end
end
diff --git a/actionpack/test/dispatch/executor_test.rb b/actionpack/test/dispatch/executor_test.rb
index 8eb6450385..5b8be39b6d 100644
--- a/actionpack/test/dispatch/executor_test.rb
+++ b/actionpack/test/dispatch/executor_test.rb
@@ -81,7 +81,7 @@ class ExecutorTest < ActiveSupport::TestCase
running = false
body.close
- assert !running
+ assert_not running
end
def test_complete_callbacks_are_called_on_close
@@ -89,7 +89,7 @@ class ExecutorTest < ActiveSupport::TestCase
executor.to_complete { completed = true }
body = call_and_return_body
- assert !completed
+ assert_not completed
body.close
assert completed
@@ -116,7 +116,7 @@ class ExecutorTest < ActiveSupport::TestCase
call_and_return_body.close
assert result
- assert !defined?(@in_shared_context) # it's not in the test itself
+ assert_not defined?(@in_shared_context) # it's not in the test itself
end
private
diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb
index 2901148a9e..a9a56f205f 100644
--- a/actionpack/test/dispatch/live_response_test.rb
+++ b/actionpack/test/dispatch/live_response_test.rb
@@ -73,7 +73,7 @@ module ActionController
}
latch.wait
- assert @response.headers.frozen?
+ assert_predicate @response.headers, :frozen?
e = assert_raises(ActionDispatch::IllegalStateError) do
@response.headers["Content-Length"] = "zomg"
end
diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb
index 90e95e972d..fa264417e1 100644
--- a/actionpack/test/dispatch/mime_type_test.rb
+++ b/actionpack/test/dispatch/mime_type_test.rb
@@ -30,21 +30,21 @@ class MimeTypeTest < ActiveSupport::TestCase
test "parse text with trailing star at the beginning" do
accept = "text/*, text/html, application/json, multipart/form-data"
- expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]]
+ expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]]
parsed = Mime::Type.parse(accept)
- assert_equal expect, parsed
+ assert_equal expect.map(&:to_s), parsed.map(&:to_s)
end
test "parse text with trailing star in the end" do
accept = "text/html, application/json, multipart/form-data, text/*"
- expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml]]
+ expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml]]
parsed = Mime::Type.parse(accept)
- assert_equal expect, parsed
+ assert_equal expect.map(&:to_s), parsed.map(&:to_s)
end
test "parse text with trailing star" do
accept = "text/*"
- expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json]]
+ expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json]]
parsed = Mime::Type.parse(accept)
assert_equal expect.map(&:to_s).sort!, parsed.map(&:to_s).sort!
end
@@ -159,7 +159,7 @@ class MimeTypeTest < ActiveSupport::TestCase
types.each do |type|
mime = Mime[type]
- assert mime.respond_to?("#{type}?"), "#{mime.inspect} does not respond to #{type}?"
+ assert_respond_to mime, "#{type}?"
assert_equal type, mime.symbol, "#{mime.inspect} is not #{type}?"
invalid_types = types - [type]
invalid_types.delete(:html)
@@ -180,8 +180,8 @@ class MimeTypeTest < ActiveSupport::TestCase
assert Mime[:js] =~ "text/javascript"
assert Mime[:js] =~ "application/javascript"
assert Mime[:js] !~ "text/html"
- assert !(Mime[:js] !~ "text/javascript")
- assert !(Mime[:js] !~ "application/javascript")
+ assert_not (Mime[:js] !~ "text/javascript")
+ assert_not (Mime[:js] !~ "application/javascript")
assert Mime[:html] =~ "application/xhtml+xml"
end
end
diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb
index e529229fae..edc4cd62a3 100644
--- a/actionpack/test/dispatch/reloader_test.rb
+++ b/actionpack/test/dispatch/reloader_test.rb
@@ -115,7 +115,7 @@ class ReloaderTest < ActiveSupport::TestCase
reloader.to_complete { completed = true }
body = call_and_return_body
- assert !completed
+ assert_not completed
body.close
assert completed
@@ -129,7 +129,7 @@ class ReloaderTest < ActiveSupport::TestCase
prepared = false
body.close
- assert !prepared
+ assert_not prepared
end
def test_complete_callbacks_are_called_on_exceptions
diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb
index 7b6ce31f29..74da2fe7d3 100644
--- a/actionpack/test/dispatch/request/session_test.rb
+++ b/actionpack/test/dispatch/request/session_test.rb
@@ -22,6 +22,7 @@ module ActionDispatch
s["foo"] = "bar"
assert_equal "bar", s["foo"]
assert_equal({ "foo" => "bar" }, s.to_hash)
+ assert_equal({ "foo" => "bar" }, s.to_h)
end
def test_create_merges_old
@@ -117,6 +118,18 @@ module ActionDispatch
end
end
+ def test_dig
+ session = Session.create(store, req, {})
+ session["one"] = { "two" => "3" }
+
+ assert_equal "3", session.dig("one", "two")
+ assert_equal "3", session.dig(:one, "two")
+
+ assert_nil session.dig("three", "two")
+ assert_nil session.dig("one", "three")
+ assert_nil session.dig("one", :two)
+ end
+
private
def store
Class.new {
diff --git a/actionpack/test/dispatch/request_id_test.rb b/actionpack/test/dispatch/request_id_test.rb
index aa3175c986..9df4712dab 100644
--- a/actionpack/test/dispatch/request_id_test.rb
+++ b/actionpack/test/dispatch/request_id_test.rb
@@ -11,6 +11,11 @@ class RequestIdTest < ActiveSupport::TestCase
assert_equal "X-Hacked-HeaderStuff", stub_request("HTTP_X_REQUEST_ID" => "; X-Hacked-Header: Stuff").request_id
end
+ test "accept Apache mod_unique_id format" do
+ mod_unique_id = "abcxyz@ABCXYZ-0123456789"
+ assert_equal mod_unique_id, stub_request("HTTP_X_REQUEST_ID" => mod_unique_id).request_id
+ end
+
test "ensure that 255 char limit on the request id is being enforced" do
assert_equal "X" * 255, stub_request("HTTP_X_REQUEST_ID" => "X" * 500).request_id
end
diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb
index 68c6d26364..84a2d1f69e 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -329,20 +329,20 @@ class RequestPort < BaseRequestTest
test "standard_port?" do
request = stub_request
- assert !request.ssl?
- assert request.standard_port?
+ assert_not_predicate request, :ssl?
+ assert_predicate request, :standard_port?
request = stub_request "HTTPS" => "on"
- assert request.ssl?
- assert request.standard_port?
+ assert_predicate request, :ssl?
+ assert_predicate request, :standard_port?
request = stub_request "HTTP_HOST" => "www.example.org:8080"
- assert !request.ssl?
- assert !request.standard_port?
+ assert_not_predicate request, :ssl?
+ assert_not_predicate request, :standard_port?
request = stub_request "HTTP_HOST" => "www.example.org:8443", "HTTPS" => "on"
- assert request.ssl?
- assert !request.standard_port?
+ assert_predicate request, :ssl?
+ assert_not_predicate request, :standard_port?
end
test "optional port" do
@@ -571,7 +571,7 @@ end
class LocalhostTest < BaseRequestTest
test "IPs that match localhost" do
request = stub_request("REMOTE_IP" => "127.1.1.1", "REMOTE_ADDR" => "127.1.1.1")
- assert request.local?
+ assert_predicate request, :local?
end
end
@@ -643,37 +643,37 @@ class RequestProtocol < BaseRequestTest
test "xml http request" do
request = stub_request
- assert !request.xml_http_request?
- assert !request.xhr?
+ assert_not_predicate request, :xml_http_request?
+ assert_not_predicate request, :xhr?
request = stub_request "HTTP_X_REQUESTED_WITH" => "DefinitelyNotAjax1.0"
- assert !request.xml_http_request?
- assert !request.xhr?
+ assert_not_predicate request, :xml_http_request?
+ assert_not_predicate request, :xhr?
request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"
- assert request.xml_http_request?
- assert request.xhr?
+ assert_predicate request, :xml_http_request?
+ assert_predicate request, :xhr?
end
test "reports ssl" do
- assert !stub_request.ssl?
- assert stub_request("HTTPS" => "on").ssl?
+ assert_not_predicate stub_request, :ssl?
+ assert_predicate stub_request("HTTPS" => "on"), :ssl?
end
test "reports ssl when proxied via lighttpd" do
- assert stub_request("HTTP_X_FORWARDED_PROTO" => "https").ssl?
+ assert_predicate stub_request("HTTP_X_FORWARDED_PROTO" => "https"), :ssl?
end
test "scheme returns https when proxied" do
request = stub_request "rack.url_scheme" => "http"
- assert !request.ssl?
+ assert_not_predicate request, :ssl?
assert_equal "http", request.scheme
request = stub_request(
"rack.url_scheme" => "http",
"HTTP_X_FORWARDED_PROTO" => "https"
)
- assert request.ssl?
+ assert_predicate request, :ssl?
assert_equal "https", request.scheme
end
end
@@ -700,7 +700,7 @@ class RequestMethod < BaseRequestTest
assert_equal "GET", request.request_method
assert_equal "GET", request.env["REQUEST_METHOD"]
- assert request.get?
+ assert_predicate request, :get?
end
test "invalid http method raises exception" do
@@ -748,7 +748,7 @@ class RequestMethod < BaseRequestTest
assert_equal "POST", request.method
assert_equal "PATCH", request.request_method
- assert request.patch?
+ assert_predicate request, :patch?
end
test "post masquerading as put" do
@@ -758,12 +758,12 @@ class RequestMethod < BaseRequestTest
)
assert_equal "POST", request.method
assert_equal "PUT", request.request_method
- assert request.put?
+ assert_predicate request, :put?
end
test "post uneffected by local inflections" do
existing_acronyms = ActiveSupport::Inflector.inflections.acronyms.dup
- existing_acronym_regex = ActiveSupport::Inflector.inflections.acronym_regex.dup
+ assert_deprecated { ActiveSupport::Inflector.inflections.acronym_regex.dup }
begin
ActiveSupport::Inflector.inflections do |inflect|
inflect.acronym "POS"
@@ -772,12 +772,12 @@ class RequestMethod < BaseRequestTest
request = stub_request "REQUEST_METHOD" => "POST"
assert_equal :post, ActionDispatch::Request::HTTP_METHOD_LOOKUP["POST"]
assert_equal :post, request.method_symbol
- assert request.post?
+ assert_predicate request, :post?
ensure
# Reset original acronym set
ActiveSupport::Inflector.inflections do |inflect|
inflect.send(:instance_variable_set, "@acronyms", existing_acronyms)
- inflect.send(:instance_variable_set, "@acronym_regex", existing_acronym_regex)
+ inflect.send(:define_acronym_regex_patterns)
end
end
end
@@ -785,50 +785,44 @@ end
class RequestFormat < BaseRequestTest
test "xml format" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: { format: :xml }) do
- assert_equal Mime[:xml], request.format
- end
+ request = stub_request "QUERY_STRING" => "format=xml"
+
+ assert_equal Mime[:xml], request.format
end
test "xhtml format" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: { format: :xhtml }) do
- assert_equal Mime[:html], request.format
- end
+ request = stub_request "QUERY_STRING" => "format=xhtml"
+
+ assert_equal Mime[:html], request.format
end
test "txt format" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: { format: :txt }) do
- assert_equal Mime[:text], request.format
- end
+ request = stub_request "QUERY_STRING" => "format=txt"
+
+ assert_equal Mime[:text], request.format
end
test "XMLHttpRequest" do
request = stub_request(
"HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
- "HTTP_ACCEPT" => [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(",")
+ "HTTP_ACCEPT" => [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(","),
+ "QUERY_STRING" => ""
)
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert request.xhr?
- assert_equal Mime[:js], request.format
- end
+ assert_predicate request, :xhr?
+ assert_equal Mime[:js], request.format
end
test "can override format with parameter negative" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: { format: :txt }) do
- assert !request.format.xml?
- end
+ request = stub_request("QUERY_STRING" => "format=txt")
+
+ assert_not_predicate request.format, :xml?
end
test "can override format with parameter positive" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: { format: :xml }) do
- assert request.format.xml?
- end
+ request = stub_request("QUERY_STRING" => "format=xml")
+
+ assert_predicate request.format, :xml?
end
test "formats text/html with accept header" do
@@ -853,40 +847,37 @@ class RequestFormat < BaseRequestTest
end
test "formats format:text with accept header" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: { format: :txt }) do
- assert_equal [Mime[:text]], request.formats
- end
+ request = stub_request("QUERY_STRING" => "format=txt")
+
+ assert_equal [Mime[:text]], request.formats
end
test "formats format:unknown with accept header" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: { format: :unknown }) do
- assert_instance_of Mime::NullType, request.format
- end
+ request = stub_request("QUERY_STRING" => "format=unknown")
+
+ assert_instance_of Mime::NullType, request.format
end
test "format is not nil with unknown format" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: { format: :hello }) do
- assert request.format.nil?
- assert_not request.format.html?
- assert_not request.format.xml?
- assert_not request.format.json?
- end
+ request = stub_request("QUERY_STRING" => "format=hello")
+
+ assert_nil request.format
+ assert_not_predicate request.format, :html?
+ assert_not_predicate request.format, :xml?
+ assert_not_predicate request.format, :json?
end
test "format does not throw exceptions when malformed parameters" do
request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2")
assert request.formats
- assert request.format.html?
+ assert_predicate request.format, :html?
end
test "formats with xhr request" do
- request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [Mime[:js]], request.formats
- end
+ request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
+ "QUERY_STRING" => ""
+
+ assert_equal [Mime[:js]], request.formats
end
test "ignore_accept_header" do
@@ -894,62 +885,58 @@ class RequestFormat < BaseRequestTest
ActionDispatch::Request.ignore_accept_header = true
begin
- request = stub_request "HTTP_ACCEPT" => "application/xml"
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime[:html] ], request.formats
- end
+ request = stub_request "HTTP_ACCEPT" => "application/xml",
+ "QUERY_STRING" => ""
- request = stub_request "HTTP_ACCEPT" => "koz-asked/something-crazy"
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime[:html] ], request.formats
- end
+ assert_equal [ Mime[:html] ], request.formats
- request = stub_request "HTTP_ACCEPT" => "*/*;q=0.1"
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime[:html] ], request.formats
- end
+ request = stub_request "HTTP_ACCEPT" => "koz-asked/something-crazy",
+ "QUERY_STRING" => ""
- request = stub_request "HTTP_ACCEPT" => "application/jxw"
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime[:html] ], request.formats
- end
+ assert_equal [ Mime[:html] ], request.formats
+
+ request = stub_request "HTTP_ACCEPT" => "*/*;q=0.1",
+ "QUERY_STRING" => ""
+
+ assert_equal [ Mime[:html] ], request.formats
+
+ request = stub_request "HTTP_ACCEPT" => "application/jxw",
+ "QUERY_STRING" => ""
+
+ assert_equal [ Mime[:html] ], request.formats
request = stub_request "HTTP_ACCEPT" => "application/xml",
- "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"
+ "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
+ "QUERY_STRING" => ""
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime[:js] ], request.formats
- end
+ assert_equal [ Mime[:js] ], request.formats
request = stub_request "HTTP_ACCEPT" => "application/xml",
- "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"
- assert_called(request, :parameters, times: 2, returns: { format: :json }) do
- assert_equal [ Mime[:json] ], request.formats
- end
+ "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
+ "QUERY_STRING" => "format=json"
+
+ assert_equal [ Mime[:json] ], request.formats
ensure
ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header
end
end
test "format taken from the path extension" do
- request = stub_request "PATH_INFO" => "/foo.xml"
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [Mime[:xml]], request.formats
- end
+ request = stub_request "PATH_INFO" => "/foo.xml", "QUERY_STRING" => ""
- request = stub_request "PATH_INFO" => "/foo.123"
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [Mime[:html]], request.formats
- end
+ assert_equal [Mime[:xml]], request.formats
+
+ request = stub_request "PATH_INFO" => "/foo.123", "QUERY_STRING" => ""
+
+ assert_equal [Mime[:html]], request.formats
end
test "formats from accept headers have higher precedence than path extension" do
request = stub_request "HTTP_ACCEPT" => "application/json",
- "PATH_INFO" => "/foo.xml"
+ "PATH_INFO" => "/foo.xml",
+ "QUERY_STRING" => ""
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [Mime[:json]], request.formats
- end
+ assert_equal [Mime[:json]], request.formats
end
end
@@ -997,15 +984,14 @@ end
class RequestParameters < BaseRequestTest
test "parameters" do
- request = stub_request
+ request = stub_request "CONTENT_TYPE" => "application/json",
+ "CONTENT_LENGTH" => 9,
+ "RAW_POST_DATA" => '{"foo":1}',
+ "QUERY_STRING" => "bar=2"
- assert_called(request, :request_parameters, times: 2, returns: { "foo" => 1 }) do
- assert_called(request, :query_parameters, times: 2, returns: { "bar" => 2 }) do
- assert_equal({ "foo" => 1, "bar" => 2 }, request.parameters)
- assert_equal({ "foo" => 1 }, request.request_parameters)
- assert_equal({ "bar" => 2 }, request.query_parameters)
- end
- end
+ assert_equal({ "foo" => 1, "bar" => "2" }, request.parameters)
+ assert_equal({ "foo" => 1 }, request.request_parameters)
+ assert_equal({ "bar" => "2" }, request.query_parameters)
end
test "parameters not accessible after rack parse error" do
@@ -1248,8 +1234,8 @@ class RequestVariant < BaseRequestTest
test "setting variant to a symbol" do
@request.variant = :phone
- assert @request.variant.phone?
- assert_not @request.variant.tablet?
+ assert_predicate @request.variant, :phone?
+ assert_not_predicate @request.variant, :tablet?
assert @request.variant.any?(:phone, :tablet)
assert_not @request.variant.any?(:tablet, :desktop)
end
@@ -1257,9 +1243,9 @@ class RequestVariant < BaseRequestTest
test "setting variant to an array of symbols" do
@request.variant = [:phone, :tablet]
- assert @request.variant.phone?
- assert @request.variant.tablet?
- assert_not @request.variant.desktop?
+ assert_predicate @request.variant, :phone?
+ assert_predicate @request.variant, :tablet?
+ assert_not_predicate @request.variant, :desktop?
assert @request.variant.any?(:tablet, :desktop)
assert_not @request.variant.any?(:desktop, :watch)
end
@@ -1267,8 +1253,8 @@ class RequestVariant < BaseRequestTest
test "clearing variant" do
@request.variant = nil
- assert @request.variant.empty?
- assert_not @request.variant.phone?
+ assert_empty @request.variant
+ assert_not_predicate @request.variant, :phone?
assert_not @request.variant.any?(:phone, :tablet)
end
@@ -1287,13 +1273,13 @@ end
class RequestFormData < BaseRequestTest
test "media_type is from the FORM_DATA_MEDIA_TYPES array" do
- assert stub_request("CONTENT_TYPE" => "application/x-www-form-urlencoded").form_data?
- assert stub_request("CONTENT_TYPE" => "multipart/form-data").form_data?
+ assert_predicate stub_request("CONTENT_TYPE" => "application/x-www-form-urlencoded"), :form_data?
+ assert_predicate stub_request("CONTENT_TYPE" => "multipart/form-data"), :form_data?
end
test "media_type is not from the FORM_DATA_MEDIA_TYPES array" do
- assert !stub_request("CONTENT_TYPE" => "application/xml").form_data?
- assert !stub_request("CONTENT_TYPE" => "multipart/related").form_data?
+ assert_not_predicate stub_request("CONTENT_TYPE" => "application/xml"), :form_data?
+ assert_not_predicate stub_request("CONTENT_TYPE" => "multipart/related"), :form_data?
end
test "no Content-Type header is provided and the request_method is POST" do
@@ -1301,6 +1287,21 @@ class RequestFormData < BaseRequestTest
assert_equal "", request.media_type
assert_equal "POST", request.request_method
- assert !request.form_data?
+ assert_not_predicate request, :form_data?
+ end
+end
+
+class EarlyHintsRequestTest < BaseRequestTest
+ def setup
+ super
+ @env["rack.early_hints"] = lambda { |links| links }
+ @request = stub_request
+ end
+
+ test "when early hints is set in the env link headers are sent" do
+ early_hints = @request.send_early_hints("Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload")
+ expected_hints = { "Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload" }
+
+ assert_equal expected_hints, early_hints
end
end
diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb
index c4ee3add2a..6d87314e97 100644
--- a/actionpack/test/dispatch/response_test.rb
+++ b/actionpack/test/dispatch/response_test.rb
@@ -15,13 +15,13 @@ class ResponseTest < ActiveSupport::TestCase
@response.await_commit
}
@response.commit!
- assert @response.committed?
+ assert_predicate @response, :committed?
assert t.join(0.5)
end
def test_stream_close
@response.stream.close
- assert @response.stream.closed?
+ assert_predicate @response.stream, :closed?
end
def test_stream_write
@@ -191,7 +191,7 @@ class ResponseTest < ActiveSupport::TestCase
test "does not include Status header" do
@response.status = "200 OK"
_, headers, _ = @response.to_a
- assert !headers.has_key?("Status")
+ assert_not headers.has_key?("Status")
end
test "response code" do
@@ -257,9 +257,9 @@ class ResponseTest < ActiveSupport::TestCase
}
resp.to_a
- assert resp.etag?
- assert resp.weak_etag?
- assert_not resp.strong_etag?
+ assert_predicate resp, :etag?
+ assert_predicate resp, :weak_etag?
+ assert_not_predicate resp, :strong_etag?
assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.etag)
assert_equal({ public: true }, resp.cache_control)
@@ -275,9 +275,9 @@ class ResponseTest < ActiveSupport::TestCase
}
resp.to_a
- assert resp.etag?
- assert_not resp.weak_etag?
- assert resp.strong_etag?
+ assert_predicate resp, :etag?
+ assert_not_predicate resp, :weak_etag?
+ assert_predicate resp, :strong_etag?
assert_equal('"202cb962ac59075b964b07152d234b70"', resp.etag)
end
@@ -311,13 +311,16 @@ class ResponseTest < ActiveSupport::TestCase
end
end
- test "read x_frame_options, x_content_type_options and x_xss_protection" do
+ test "read x_frame_options, x_content_type_options, x_xss_protection, x_download_options and x_permitted_cross_domain_policies, referrer_policy" do
original_default_headers = ActionDispatch::Response.default_headers
begin
ActionDispatch::Response.default_headers = {
"X-Frame-Options" => "DENY",
"X-Content-Type-Options" => "nosniff",
- "X-XSS-Protection" => "1;"
+ "X-XSS-Protection" => "1;",
+ "X-Download-Options" => "noopen",
+ "X-Permitted-Cross-Domain-Policies" => "none",
+ "Referrer-Policy" => "strict-origin-when-cross-origin"
}
resp = ActionDispatch::Response.create.tap { |response|
response.body = "Hello"
@@ -327,6 +330,9 @@ class ResponseTest < ActiveSupport::TestCase
assert_equal("DENY", resp.headers["X-Frame-Options"])
assert_equal("nosniff", resp.headers["X-Content-Type-Options"])
assert_equal("1;", resp.headers["X-XSS-Protection"])
+ assert_equal("noopen", resp.headers["X-Download-Options"])
+ assert_equal("none", resp.headers["X-Permitted-Cross-Domain-Policies"])
+ assert_equal("strict-origin-when-cross-origin", resp.headers["Referrer-Policy"])
ensure
ActionDispatch::Response.default_headers = original_default_headers
end
@@ -350,7 +356,7 @@ class ResponseTest < ActiveSupport::TestCase
end
test "respond_to? accepts include_private" do
- assert_not @response.respond_to?(:method_missing)
+ assert_not_respond_to @response, :method_missing
assert @response.respond_to?(:method_missing, true)
end
diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb
index 438a918567..9150d5010b 100644
--- a/actionpack/test/dispatch/routing/inspector_test.rb
+++ b/actionpack/test/dispatch/routing/inspector_test.rb
@@ -3,6 +3,7 @@
require "abstract_unit"
require "rails/engine"
require "action_dispatch/routing/inspector"
+require "io/console/size"
class MountedRackApp
def self.call(env)
@@ -15,16 +16,10 @@ end
module ActionDispatch
module Routing
class RoutesInspectorTest < ActiveSupport::TestCase
- def setup
+ setup do
@set = ActionDispatch::Routing::RouteSet.new
end
- def draw(options = nil, &block)
- @set.draw(&block)
- inspector = ActionDispatch::Routing::RoutesInspector.new(@set.routes)
- inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, options).split("\n")
- end
-
def test_displaying_routes_for_engines
engine = Class.new(Rails::Engine) do
def self.inspect
@@ -305,7 +300,7 @@ module ActionDispatch
end
def test_routes_can_be_filtered
- output = draw("posts") do
+ output = draw(grep: "posts") do
resources :articles
resources :posts
end
@@ -321,8 +316,76 @@ module ActionDispatch
" DELETE /posts/:id(.:format) posts#destroy"], output
end
+ def test_routes_when_expanded
+ previous_console_winsize = IO.console.winsize
+ IO.console.winsize = [0, 23]
+
+ engine = Class.new(Rails::Engine) do
+ def self.inspect
+ "Blog::Engine"
+ end
+ end
+ engine.routes.draw do
+ get "/cart", to: "cart#show"
+ end
+
+ output = draw(formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) do
+ get "/custom/assets", to: "custom_assets#show"
+ get "/custom/furnitures", to: "custom_furnitures#show"
+ mount engine => "/blog", :as => "blog"
+ end
+
+ assert_equal ["--[ Route 1 ]----------",
+ "Prefix | custom_assets",
+ "Verb | GET",
+ "URI | /custom/assets(.:format)",
+ "Controller#Action | custom_assets#show",
+ "--[ Route 2 ]----------",
+ "Prefix | custom_furnitures",
+ "Verb | GET",
+ "URI | /custom/furnitures(.:format)",
+ "Controller#Action | custom_furnitures#show",
+ "--[ Route 3 ]----------",
+ "Prefix | blog",
+ "Verb | ",
+ "URI | /blog",
+ "Controller#Action | Blog::Engine",
+ "",
+ "[ Routes for Blog::Engine ]",
+ "--[ Route 1 ]----------",
+ "Prefix | cart",
+ "Verb | GET",
+ "URI | /cart(.:format)",
+ "Controller#Action | cart#show"], output
+ ensure
+ IO.console.winsize = previous_console_winsize
+ end
+
+ def test_no_routes_matched_filter_when_expanded
+ output = draw(grep: "rails/dummy", formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) do
+ get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/
+ end
+
+ assert_equal [
+ "No routes were found for this grep pattern.",
+ "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html."
+ ], output
+ end
+
+ def test_not_routes_when_expanded
+ output = draw(grep: "rails/dummy", formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) {}
+
+ assert_equal [
+ "You don't have any routes defined!",
+ "",
+ "Please add some routes in config/routes.rb.",
+ "",
+ "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html."
+ ], output
+ end
+
def test_routes_can_be_filtered_with_namespaced_controllers
- output = draw("admin/posts") do
+ output = draw(grep: "admin/posts") do
resources :articles
namespace :admin do
resources :posts
@@ -370,24 +433,24 @@ module ActionDispatch
end
assert_equal [
- "No routes were found for this controller",
+ "No routes were found for this controller.",
"For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html."
], output
end
def test_no_routes_matched_filter
- output = draw("rails/dummy") do
+ output = draw(grep: "rails/dummy") do
get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/
end
assert_equal [
- "No routes were found for this controller",
+ "No routes were found for this grep pattern.",
"For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html."
], output
end
def test_no_routes_were_defined
- output = draw("Rails::DummyController") {}
+ output = draw(grep: "Rails::DummyController") {}
assert_equal [
"You don't have any routes defined!",
@@ -420,6 +483,13 @@ module ActionDispatch
"custom_assets GET /custom/assets(.:format) custom_assets#show",
], output
end
+
+ private
+ def draw(formatter: ActionDispatch::Routing::ConsoleFormatter::Sheet.new, **options, &block)
+ @set.draw(&block)
+ inspector = ActionDispatch::Routing::RoutesInspector.new(@set.routes)
+ inspector.format(formatter, options).split("\n")
+ end
end
end
end
diff --git a/actionpack/test/dispatch/routing_assertions_test.rb b/actionpack/test/dispatch/routing_assertions_test.rb
index e492a56653..009b6d9bc3 100644
--- a/actionpack/test/dispatch/routing_assertions_test.rb
+++ b/actionpack/test/dispatch/routing_assertions_test.rb
@@ -1,14 +1,40 @@
# frozen_string_literal: true
require "abstract_unit"
+require "rails/engine"
require "controller/fake_controllers"
class SecureArticlesController < ArticlesController; end
class BlockArticlesController < ArticlesController; end
class QueryArticlesController < ArticlesController; end
+class SecureBooksController < BooksController; end
+class BlockBooksController < BooksController; end
+class QueryBooksController < BooksController; end
+
class RoutingAssertionsTest < ActionController::TestCase
def setup
+ engine = Class.new(Rails::Engine) do
+ def self.name
+ "blog_engine"
+ end
+ end
+ engine.routes.draw do
+ resources :books
+
+ scope "secure", constraints: { protocol: "https://" } do
+ resources :books, controller: "secure_books"
+ end
+
+ scope "block", constraints: lambda { |r| r.ssl? } do
+ resources :books, controller: "block_books"
+ end
+
+ scope "query", constraints: lambda { |r| r.params[:use_query] == "true" } do
+ resources :books, controller: "query_books"
+ end
+ end
+
@routes = ActionDispatch::Routing::RouteSet.new
@routes.draw do
resources :articles
@@ -24,6 +50,10 @@ class RoutingAssertionsTest < ActionController::TestCase
scope "query", constraints: lambda { |r| r.params[:use_query] == "true" } do
resources :articles, controller: "query_articles"
end
+
+ mount engine => "/shelf"
+
+ get "/shelf/foo", controller: "query_articles", action: "index"
end
end
@@ -83,6 +113,53 @@ class RoutingAssertionsTest < ActionController::TestCase
assert_match err.message, "This is a really bad msg"
end
+ def test_assert_recognizes_with_engine
+ assert_recognizes({ controller: "books", action: "index" }, "/shelf/books")
+ assert_recognizes({ controller: "books", action: "show", id: "1" }, "/shelf/books/1")
+ end
+
+ def test_assert_recognizes_with_engine_and_extras
+ assert_recognizes({ controller: "books", action: "index", page: "1" }, "/shelf/books", page: "1")
+ end
+
+ def test_assert_recognizes_with_engine_and_method
+ assert_recognizes({ controller: "books", action: "create" }, { path: "/shelf/books", method: :post })
+ assert_recognizes({ controller: "books", action: "update", id: "1" }, { path: "/shelf/books/1", method: :put })
+ end
+
+ def test_assert_recognizes_with_engine_and_hash_constraint
+ assert_raise(Assertion) do
+ assert_recognizes({ controller: "secure_books", action: "index" }, "http://test.host/shelf/secure/books")
+ end
+ assert_recognizes({ controller: "secure_books", action: "index", protocol: "https://" }, "https://test.host/shelf/secure/books")
+ end
+
+ def test_assert_recognizes_with_engine_and_block_constraint
+ assert_raise(Assertion) do
+ assert_recognizes({ controller: "block_books", action: "index" }, "http://test.host/shelf/block/books")
+ end
+ assert_recognizes({ controller: "block_books", action: "index" }, "https://test.host/shelf/block/books")
+ end
+
+ def test_assert_recognizes_with_engine_and_query_constraint
+ assert_raise(Assertion) do
+ assert_recognizes({ controller: "query_books", action: "index", use_query: "false" }, "/shelf/query/books", use_query: "false")
+ end
+ assert_recognizes({ controller: "query_books", action: "index", use_query: "true" }, "/shelf/query/books", use_query: "true")
+ end
+
+ def test_assert_recognizes_raises_message_with_engine
+ err = assert_raise(Assertion) do
+ assert_recognizes({ controller: "secure_books", action: "index" }, "http://test.host/shelf/secure/books", {}, "This is a really bad msg")
+ end
+
+ assert_match err.message, "This is a really bad msg"
+ end
+
+ def test_assert_recognizes_continue_to_recoginize_after_it_tried_engines
+ assert_recognizes({ controller: "query_articles", action: "index" }, "/shelf/foo")
+ end
+
def test_assert_routing
assert_routing("/articles", controller: "articles", action: "index")
end
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index 446b65a9b9..dd6adcbfd1 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -3,6 +3,7 @@
require "erb"
require "abstract_unit"
require "controller/fake_controllers"
+require "active_support/messages/rotation_configuration"
class TestRoutingMapper < ActionDispatch::IntegrationTest
SprocketsApp = lambda { |env|
@@ -3165,7 +3166,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- assert !respond_to?(:routes_no_collision_path)
+ assert_not respond_to?(:routes_no_collision_path)
end
def test_controller_name_with_leading_slash_raise_error
@@ -3312,7 +3313,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
get "/search"
- assert !@request.params[:action].frozen?
+ assert_not_predicate @request.params[:action], :frozen?
end
def test_multiple_positional_args_with_the_same_name
@@ -4224,7 +4225,7 @@ class TestGlobRoutingMapper < ActionDispatch::IntegrationTest
end
end
- #include Routes.url_helpers
+ # include Routes.url_helpers
APP = build_app Routes
def app; APP end
@@ -4266,7 +4267,7 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest
def app; APP end
test "enabled when not mounted and default_url_options is empty" do
- assert Routes.url_helpers.optimize_routes_generation?
+ assert_predicate Routes.url_helpers, :optimize_routes_generation?
end
test "named route called as singleton method" do
@@ -4499,7 +4500,7 @@ class TestPortConstraints < ActionDispatch::IntegrationTest
get "/integer", to: ok, constraints: { port: 8080 }
get "/string", to: ok, constraints: { port: "8080" }
- get "/array", to: ok, constraints: { port: [8080] }
+ get "/array/:idx", to: ok, constraints: { port: [8080], idx: %w[first last] }
get "/regexp", to: ok, constraints: { port: /8080/ }
end
end
@@ -4528,7 +4529,10 @@ class TestPortConstraints < ActionDispatch::IntegrationTest
get "http://www.example.com/array"
assert_response :not_found
- get "http://www.example.com:8080/array"
+ get "http://www.example.com:8080/array/middle"
+ assert_response :not_found
+
+ get "http://www.example.com:8080/array/first"
assert_response :success
end
@@ -4947,6 +4951,7 @@ end
class FlashRedirectTest < ActionDispatch::IntegrationTest
SessionKey = "_myapp_session"
Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33")
+ Rotations = ActiveSupport::Messages::RotationConfiguration.new
class KeyGeneratorMiddleware
def initialize(app)
@@ -4955,6 +4960,8 @@ class FlashRedirectTest < ActionDispatch::IntegrationTest
def call(env)
env["action_dispatch.key_generator"] ||= Generator
+ env["action_dispatch.cookies_rotations"] ||= Rotations
+
@app.call(env)
end
end
@@ -5053,3 +5060,40 @@ class TestRecognizePath < ActionDispatch::IntegrationTest
Routes.recognize_path(*args)
end
end
+
+class TestRelativeUrlRootGeneration < ActionDispatch::IntegrationTest
+ config = ActionDispatch::Routing::RouteSet::Config.new("/blog", false)
+
+ stub_controllers(config) do |routes|
+ Routes = routes
+
+ routes.draw do
+ get "/", to: "posts#index", as: :posts
+ get "/:id", to: "posts#show", as: :post
+ end
+ end
+
+ include Routes.url_helpers
+
+ APP = build_app Routes
+
+ def app
+ APP
+ end
+
+ def test_url_helpers
+ assert_equal "/blog/", posts_path({})
+ assert_equal "/blog/", Routes.url_helpers.posts_path({})
+
+ assert_equal "/blog/1", post_path(id: "1")
+ assert_equal "/blog/1", Routes.url_helpers.post_path(id: "1")
+ end
+
+ def test_optimized_url_helpers
+ assert_equal "/blog/", posts_path
+ assert_equal "/blog/", Routes.url_helpers.posts_path
+
+ assert_equal "/blog/1", post_path("1")
+ assert_equal "/blog/1", Routes.url_helpers.post_path("1")
+ end
+end
diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb
index 6517cf4c99..e34426a471 100644
--- a/actionpack/test/dispatch/session/cookie_store_test.rb
+++ b/actionpack/test/dispatch/session/cookie_store_test.rb
@@ -3,14 +3,19 @@
require "abstract_unit"
require "stringio"
require "active_support/key_generator"
+require "active_support/messages/rotation_configuration"
class CookieStoreTest < ActionDispatch::IntegrationTest
SessionKey = "_myapp_session"
SessionSecret = "b3c631c314c0bbca50c1b2843150fe33"
- Generator = ActiveSupport::LegacyKeyGenerator.new(SessionSecret)
+ SessionSalt = "authenticated encrypted cookie"
- Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, digest: "SHA1")
- SignedBar = Verifier.generate(foo: "bar", session_id: SecureRandom.hex(16))
+ Generator = ActiveSupport::KeyGenerator.new(SessionSecret, iterations: 1000)
+ Rotations = ActiveSupport::Messages::RotationConfiguration.new
+
+ Encryptor = ActiveSupport::MessageEncryptor.new(
+ Generator.generate_key(SessionSalt, 32), cipher: "aes-256-gcm", serializer: Marshal
+ )
class TestController < ActionController::Base
def no_session_access
@@ -23,12 +28,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def set_session_value
session[:foo] = "bar"
- render plain: Rack::Utils.escape(Verifier.generate(session.to_hash))
- end
-
- def set_session_value_expires_in_five_hours
- session[:foo] = "bar"
- render plain: Rack::Utils.escape(Verifier.generate(session.to_hash, expires_in: 5.hours))
+ render body: nil
end
def get_session_value
@@ -70,19 +70,35 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
end
end
+ def parse_cookie_from_header
+ cookie_matches = headers["Set-Cookie"].match(/#{SessionKey}=([^;]+)/)
+ cookie_matches && cookie_matches[1]
+ end
+
+ def assert_session_cookie(cookie_string, contents)
+ assert_includes headers["Set-Cookie"], cookie_string
+
+ session_value = parse_cookie_from_header
+ session_data = Encryptor.decrypt_and_verify(Rack::Utils.unescape(session_value)) rescue nil
+
+ assert_not_nil session_data, "session failed to decrypt"
+ assert_equal session_data.slice(*contents.keys), contents
+ end
+
def test_setting_session_value
with_test_route_set do
get "/set_session_value"
+
assert_response :success
- assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
- headers["Set-Cookie"]
+ assert_session_cookie "path=/; HttpOnly", "foo" => "bar"
end
end
def test_getting_session_value
with_test_route_set do
- cookies[SessionKey] = SignedBar
+ get "/set_session_value"
get "/get_session_value"
+
assert_response :success
assert_equal 'foo: "bar"', response.body
end
@@ -90,8 +106,9 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_getting_session_id
with_test_route_set do
- cookies[SessionKey] = SignedBar
+ get "/set_session_value"
get "/persistent_session_id"
+
assert_response :success
assert_equal 32, response.body.size
session_id = response.body
@@ -104,8 +121,12 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_disregards_tampered_sessions
with_test_route_set do
- cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--123456780"
+ encryptor = ActiveSupport::MessageEncryptor.new("A" * 32, cipher: "aes-256-gcm", serializer: Marshal)
+
+ cookies[SessionKey] = encryptor.encrypt_and_sign("foo" => "bar", "session_id" => "abc")
+
get "/get_session_value"
+
assert_response :success
assert_equal "foo: nil", response.body
end
@@ -133,19 +154,19 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_does_set_secure_cookies_over_https
with_test_route_set(secure: true) do
get "/set_session_value", headers: { "HTTPS" => "on" }
+
assert_response :success
- assert_equal "_myapp_session=#{response.body}; path=/; secure; HttpOnly",
- headers["Set-Cookie"]
+ assert_session_cookie "path=/; secure; HttpOnly", "foo" => "bar"
end
end
# {:foo=>#<SessionAutoloadTest::Foo bar:"baz">, :session_id=>"ce8b0752a6ab7c7af3cdb8a80e6b9e46"}
- SignedSerializedCookie = "BAh7BzoIZm9vbzodU2Vzc2lvbkF1dG9sb2FkVGVzdDo6Rm9vBjoJQGJhciIIYmF6Og9zZXNzaW9uX2lkIiVjZThiMDc1MmE2YWI3YzdhZjNjZGI4YTgwZTZiOWU0Ng==--2bf3af1ae8bd4e52b9ac2099258ace0c380e601c"
+ EncryptedSerializedCookie = "9RZ2Fij0qLveUwM4s+CCjGqhpjyUC8jiBIf/AiBr9M3TB8xh2vQZtvSOMfN3uf6oYbbpIDHAcOFIEl69FcW1ozQYeSrCLonYCazoh34ZdYskIQfGwCiSYleVXG1OD9Z4jFqeVArw4Ewm0paOOPLbN1rc6A==--I359v/KWdZ1ok0ey--JFFhuPOY7WUo6tB/eP05Aw=="
def test_deserializes_unloaded_classes_on_get_id
with_test_route_set do
with_autoload_path "session_autoload_test" do
- cookies[SessionKey] = SignedSerializedCookie
+ cookies[SessionKey] = EncryptedSerializedCookie
get "/get_session_id"
assert_response :success
assert_equal "id: ce8b0752a6ab7c7af3cdb8a80e6b9e46", response.body, "should auto-load unloaded class"
@@ -156,7 +177,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_deserializes_unloaded_classes_on_get_value
with_test_route_set do
with_autoload_path "session_autoload_test" do
- cookies[SessionKey] = SignedSerializedCookie
+ cookies[SessionKey] = EncryptedSerializedCookie
get "/get_session_value"
assert_response :success
assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class"
@@ -195,8 +216,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
get "/set_session_value"
assert_response :success
session_payload = response.body
- assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
- headers["Set-Cookie"]
+ assert_session_cookie "path=/; HttpOnly", "foo" => "bar"
get "/call_reset_session"
assert_response :success
@@ -214,8 +234,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
with_test_route_set do
get "/set_session_value"
assert_response :success
- assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
- headers["Set-Cookie"]
+ assert_session_cookie "path=/; HttpOnly", "foo" => "bar"
get "/get_class_after_reset_session"
assert_response :success
@@ -237,8 +256,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
with_test_route_set do
get "/set_session_value"
assert_response :success
- assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
- headers["Set-Cookie"]
+ assert_session_cookie "path=/; HttpOnly", "foo" => "bar"
get "/call_session_clear"
assert_response :success
@@ -251,7 +269,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_persistent_session_id
with_test_route_set do
- cookies[SessionKey] = SignedBar
+ get "/set_session_value"
get "/persistent_session_id"
assert_response :success
assert_equal 32, response.body.size
@@ -266,8 +284,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_setting_session_id_to_nil_is_respected
with_test_route_set do
- cookies[SessionKey] = SignedBar
-
+ get "/set_session_value"
get "/get_session_id"
sid = response.body
assert_equal 36, sid.size
@@ -281,31 +298,53 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
with_test_route_set(expire_after: 5.hours) do
# First request accesses the session
time = Time.local(2008, 4, 24)
- cookie_body = nil
Time.stub :now, time do
expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
- cookies[SessionKey] = SignedBar
+ get "/set_session_value"
- get "/set_session_value_expires_in_five_hours"
assert_response :success
-
- cookie_body = response.body
- assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly",
- headers["Set-Cookie"]
+ assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar"
end
# Second request does not access the session
- time = Time.local(2008, 4, 25)
+ time = time + 3.hours
Time.stub :now, time do
expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
get "/no_session_access"
+
assert_response :success
+ assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar"
+ end
+ end
+ end
+
+ def test_session_store_with_expire_after_does_not_accept_expired_session
+ with_test_route_set(expire_after: 5.hours) do
+ # First request accesses the session
+ time = Time.local(2017, 11, 12)
+
+ Time.stub :now, time do
+ expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
+
+ get "/set_session_value"
+ get "/get_session_value"
- assert_equal "_myapp_session=#{cookies[SessionKey]}; path=/; expires=#{expected_expiry}; HttpOnly",
- headers["Set-Cookie"]
+ assert_response :success
+ assert_equal 'foo: "bar"', response.body
+ assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar"
+ end
+
+ # Second request is beyond the expiry time and the session is invalidated
+ time += 5.hours + 1.minute
+
+ Time.stub :now, time do
+ get "/get_session_value"
+
+ assert_response :success
+ assert_equal "foo: nil", response.body
end
end
end
@@ -345,7 +384,15 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def get(path, *args)
args[0] ||= {}
args[0][:headers] ||= {}
- args[0][:headers]["action_dispatch.key_generator"] ||= Generator
+ args[0][:headers].tap do |config|
+ config["action_dispatch.secret_key_base"] = SessionSecret
+ config["action_dispatch.authenticated_encrypted_cookie_salt"] = SessionSalt
+ config["action_dispatch.use_authenticated_cookie_encryption"] = true
+
+ config["action_dispatch.key_generator"] ||= Generator
+ config["action_dispatch.cookies_rotations"] ||= Rotations
+ end
+
super(path, *args)
end
diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb
index 8ac9502af9..baf46e7c7e 100644
--- a/actionpack/test/dispatch/ssl_test.rb
+++ b/actionpack/test/dispatch/ssl_test.rb
@@ -98,8 +98,8 @@ class RedirectSSLTest < SSLTest
end
class StrictTransportSecurityTest < SSLTest
- EXPECTED = "max-age=15552000"
- EXPECTED_WITH_SUBDOMAINS = "max-age=15552000; includeSubDomains"
+ EXPECTED = "max-age=31536000"
+ EXPECTED_WITH_SUBDOMAINS = "max-age=31536000; includeSubDomains"
def assert_hsts(expected, url: "https://example.org", hsts: { subdomains: true }, headers: {})
self.app = build_app ssl_options: { hsts: hsts }, headers: headers
@@ -208,6 +208,14 @@ class SecureCookiesTest < SSLTest
assert_cookies(*DEFAULT.split("\n"))
end
+ def test_cookies_as_not_secure_with_exclude
+ excluding = { exclude: -> request { request.domain =~ /example/ } }
+ get headers: { "Set-Cookie" => DEFAULT }, ssl_options: { redirect: excluding }
+
+ assert_cookies(*DEFAULT.split("\n"))
+ assert_response :ok
+ end
+
def test_no_cookies
get
assert_nil response.headers["Set-Cookie"]
diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb
index 0bdff68692..6b69cd9999 100644
--- a/actionpack/test/dispatch/static_test.rb
+++ b/actionpack/test/dispatch/static_test.rb
@@ -71,7 +71,16 @@ module StaticTests
end
def test_served_static_file_with_non_english_filename
- assert_html "means hello in Japanese\n", get("/foo/#{Rack::Utils.escape("こんにちは.html")}")
+ assert_html "means hello in Japanese\n", get("/foo/%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF.html")
+ end
+
+ def test_served_gzipped_static_file_with_non_english_filename
+ response = get("/foo/%E3%81%95%E3%82%88%E3%81%86%E3%81%AA%E3%82%89.html", "HTTP_ACCEPT_ENCODING" => "gzip")
+
+ assert_gzip "/foo/さようなら.html", response
+ assert_equal "text/html", response.headers["Content-Type"]
+ assert_equal "Accept-Encoding", response.headers["Vary"]
+ assert_equal "gzip", response.headers["Content-Encoding"]
end
def test_serves_static_file_with_exclamation_mark_in_filename
diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb
index e6f9353b22..a824ee0c84 100644
--- a/actionpack/test/dispatch/system_testing/driver_test.rb
+++ b/actionpack/test/dispatch/system_testing/driver_test.rb
@@ -12,7 +12,24 @@ class DriverTest < ActiveSupport::TestCase
test "initializing the driver with a browser" do
driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :chrome, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" })
assert_equal :selenium, driver.instance_variable_get(:@name)
- assert_equal :chrome, driver.instance_variable_get(:@browser)
+ assert_equal :chrome, driver.instance_variable_get(:@browser).name
+ assert_nil driver.instance_variable_get(:@browser).options
+ assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size)
+ assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options)
+ end
+
+ test "initializing the driver with a headless chrome" do
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_chrome, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" })
+ assert_equal :selenium, driver.instance_variable_get(:@name)
+ assert_equal :headless_chrome, driver.instance_variable_get(:@browser).name
+ assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size)
+ assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options)
+ end
+
+ test "initializing the driver with a headless firefox" do
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_firefox, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" })
+ assert_equal :selenium, driver.instance_variable_get(:@name)
+ assert_equal :headless_firefox, driver.instance_variable_get(:@browser).name
assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size)
assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options)
end
diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
index 2afda31cf5..de79c05657 100644
--- a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
+++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
@@ -9,7 +9,7 @@ class ScreenshotHelperTest < ActiveSupport::TestCase
new_test = DrivenBySeleniumWithChrome.new("x")
Rails.stub :root, Pathname.getwd do
- assert_equal "tmp/screenshots/x.png", new_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path)
end
end
@@ -18,7 +18,7 @@ class ScreenshotHelperTest < ActiveSupport::TestCase
Rails.stub :root, Pathname.getwd do
new_test.stub :passed?, false do
- assert_equal "tmp/screenshots/failures_x.png", new_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/failures_x.png").to_s, new_test.send(:image_path)
end
end
end
@@ -29,12 +29,17 @@ class ScreenshotHelperTest < ActiveSupport::TestCase
Rails.stub :root, Pathname.getwd do
new_test.stub :passed?, false do
new_test.stub :skipped?, true do
- assert_equal "tmp/screenshots/x.png", new_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path)
end
end
end
end
+ test "defaults to simple output for the screenshot" do
+ new_test = DrivenBySeleniumWithChrome.new("x")
+ assert_equal "simple", new_test.send(:output_type)
+ end
+
test "display_image return artifact format when specify RAILS_SYSTEM_TESTING_SCREENSHOT environment" do
begin
original_output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"]
@@ -42,6 +47,8 @@ class ScreenshotHelperTest < ActiveSupport::TestCase
new_test = DrivenBySeleniumWithChrome.new("x")
+ assert_equal "artifact", new_test.send(:output_type)
+
Rails.stub :root, Pathname.getwd do
new_test.stub :passed?, false do
assert_match %r|url=artifact://.+?tmp/screenshots/failures_x\.png|, new_test.send(:display_image)
@@ -52,11 +59,11 @@ class ScreenshotHelperTest < ActiveSupport::TestCase
end
end
- test "image path returns the relative path from current directory" do
+ test "image path returns the absolute path from root" do
new_test = DrivenBySeleniumWithChrome.new("x")
Rails.stub :root, Pathname.getwd.join("..") do
- assert_equal "../tmp/screenshots/x.png", new_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path)
end
end
end
diff --git a/actionpack/test/dispatch/system_testing/server_test.rb b/actionpack/test/dispatch/system_testing/server_test.rb
index ed65d93e49..740e90a4da 100644
--- a/actionpack/test/dispatch/system_testing/server_test.rb
+++ b/actionpack/test/dispatch/system_testing/server_test.rb
@@ -6,14 +6,27 @@ require "action_dispatch/system_testing/server"
class ServerTest < ActiveSupport::TestCase
setup do
+ @old_capybara_server = Capybara.server
+ end
+
+ test "port is always included" do
+ ActionDispatch::SystemTesting::Server.new.run
+ assert Capybara.always_include_port, "expected Capybara.always_include_port to be true"
+ end
+
+ test "server is changed from `default` to `puma`" do
+ Capybara.server = :default
ActionDispatch::SystemTesting::Server.new.run
+ assert_not_equal Capybara.server, Capybara.servers[:default]
end
- test "initializing the server port" do
- assert_includes Capybara.servers, :rails_puma
+ test "server is not changed to `puma` when is different than default" do
+ Capybara.server = :webrick
+ ActionDispatch::SystemTesting::Server.new.run
+ assert_equal Capybara.server, Capybara.servers[:webrick]
end
- test "port is always included" do
- assert Capybara.always_include_port, "expected Capybara.always_include_port to be true"
+ teardown do
+ Capybara.server = @old_capybara_server
end
end
diff --git a/actionpack/test/dispatch/system_testing/system_test_case_test.rb b/actionpack/test/dispatch/system_testing/system_test_case_test.rb
index b60ec559ae..b078a5abc5 100644
--- a/actionpack/test/dispatch/system_testing/system_test_case_test.rb
+++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb
@@ -22,6 +22,18 @@ class SetDriverToSeleniumTest < DrivenBySeleniumWithChrome
end
end
+class SetDriverToSeleniumHeadlessChromeTest < DrivenBySeleniumWithHeadlessChrome
+ test "uses selenium headless chrome" do
+ assert_equal :selenium, Capybara.current_driver
+ end
+end
+
+class SetDriverToSeleniumHeadlessFirefoxTest < DrivenBySeleniumWithHeadlessFirefox
+ test "uses selenium headless firefox" do
+ assert_equal :selenium, Capybara.current_driver
+ end
+end
+
class SetHostTest < DrivenByRackTest
test "sets default host" do
assert_equal "http://127.0.0.1", Capybara.app_host
@@ -36,32 +48,37 @@ end
class UndefMethodsTest < DrivenBySeleniumWithChrome
test "get" do
- assert_raise NoMethodError do
+ exception = assert_raise NoMethodError do
get "http://example.com"
end
+ assert_equal "System tests cannot make direct requests via #get; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message
end
test "post" do
- assert_raise NoMethodError do
+ exception = assert_raise NoMethodError do
post "http://example.com"
end
+ assert_equal "System tests cannot make direct requests via #post; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message
end
test "put" do
- assert_raise NoMethodError do
+ exception = assert_raise NoMethodError do
put "http://example.com"
end
+ assert_equal "System tests cannot make direct requests via #put; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message
end
test "patch" do
- assert_raise NoMethodError do
+ exception = assert_raise NoMethodError do
patch "http://example.com"
end
+ assert_equal "System tests cannot make direct requests via #patch; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message
end
test "delete" do
- assert_raise NoMethodError do
+ exception = assert_raise NoMethodError do
delete "http://example.com"
end
+ assert_equal "System tests cannot make direct requests via #delete; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message
end
end
diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb
index 4673d7cc11..5a584b12e5 100644
--- a/actionpack/test/dispatch/uploaded_file_test.rb
+++ b/actionpack/test/dispatch/uploaded_file_test.rb
@@ -18,7 +18,7 @@ module ActionDispatch
def test_filename_is_different_object
file_str = "foo"
uf = Http::UploadedFile.new(filename: file_str, tempfile: Object.new)
- assert_not_equal file_str.object_id , uf.original_filename.object_id
+ assert_not_equal file_str.object_id, uf.original_filename.object_id
end
def test_filename_should_be_in_utf_8
@@ -100,14 +100,14 @@ module ActionDispatch
def test_delegate_eof_to_tempfile
tf = Class.new { def eof?; true end; }
uf = Http::UploadedFile.new(tempfile: tf.new)
- assert uf.eof?
+ assert_predicate uf, :eof?
end
def test_respond_to?
tf = Class.new { def read; yield end }
uf = Http::UploadedFile.new(tempfile: tf.new)
- assert uf.respond_to?(:headers), "responds to headers"
- assert uf.respond_to?(:read), "responds to read"
+ assert_respond_to uf, :headers
+ assert_respond_to uf, :read
end
end
end
diff --git a/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb b/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb
new file mode 100644
index 0000000000..aad73c0d6b
--- /dev/null
+++ b/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb
@@ -0,0 +1 @@
+<p>Hello!</p>
diff --git a/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder b/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder
new file mode 100644
index 0000000000..2bdda3af18
--- /dev/null
+++ b/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder
@@ -0,0 +1,5 @@
+cache do
+ xml.title "Hello!"
+end
+
+xml.body cdata_section(render("formatted_partial"))
diff --git a/actionpack/test/fixtures/public/foo/さようなら.html b/actionpack/test/fixtures/public/foo/さようなら.html
new file mode 100644
index 0000000000..627bb2469f
--- /dev/null
+++ b/actionpack/test/fixtures/public/foo/さようなら.html
@@ -0,0 +1 @@
+means goodbye in Japanese
diff --git a/actionpack/test/fixtures/public/foo/さようなら.html.gz b/actionpack/test/fixtures/public/foo/さようなら.html.gz
new file mode 100644
index 0000000000..4f484cfe86
--- /dev/null
+++ b/actionpack/test/fixtures/public/foo/さようなら.html.gz
Binary files differ
diff --git a/actionpack/test/fixtures/公共/foo/さようなら.html b/actionpack/test/fixtures/公共/foo/さようなら.html
new file mode 100644
index 0000000000..627bb2469f
--- /dev/null
+++ b/actionpack/test/fixtures/公共/foo/さようなら.html
@@ -0,0 +1 @@
+means goodbye in Japanese
diff --git a/actionpack/test/fixtures/公共/foo/さようなら.html.gz b/actionpack/test/fixtures/公共/foo/さようなら.html.gz
new file mode 100644
index 0000000000..4f484cfe86
--- /dev/null
+++ b/actionpack/test/fixtures/公共/foo/さようなら.html.gz
Binary files differ
diff --git a/actionpack/test/journey/nodes/symbol_test.rb b/actionpack/test/journey/nodes/symbol_test.rb
index 1e687acef2..b0622ac71a 100644
--- a/actionpack/test/journey/nodes/symbol_test.rb
+++ b/actionpack/test/journey/nodes/symbol_test.rb
@@ -8,10 +8,10 @@ module ActionDispatch
class TestSymbol < ActiveSupport::TestCase
def test_default_regexp?
sym = Symbol.new "foo"
- assert sym.default_regexp?
+ assert_predicate sym, :default_regexp?
sym.regexp = nil
- assert_not sym.default_regexp?
+ assert_not_predicate sym, :default_regexp?
end
end
end
diff --git a/actionpack/test/journey/route/definition/scanner_test.rb b/actionpack/test/journey/route/definition/scanner_test.rb
index 070886c7df..bcbe4388c3 100644
--- a/actionpack/test/journey/route/definition/scanner_test.rb
+++ b/actionpack/test/journey/route/definition/scanner_test.rb
@@ -10,61 +10,64 @@ module ActionDispatch
@scanner = Scanner.new
end
- # /page/:id(/:action)(.:format)
- def test_tokens
- [
- ["/", [[:SLASH, "/"]]],
- ["*omg", [[:STAR, "*omg"]]],
- ["/page", [[:SLASH, "/"], [:LITERAL, "page"]]],
- ["/page!", [[:SLASH, "/"], [:LITERAL, "page!"]]],
- ["/page$", [[:SLASH, "/"], [:LITERAL, "page$"]]],
- ["/page&", [[:SLASH, "/"], [:LITERAL, "page&"]]],
- ["/page'", [[:SLASH, "/"], [:LITERAL, "page'"]]],
- ["/page*", [[:SLASH, "/"], [:LITERAL, "page*"]]],
- ["/page+", [[:SLASH, "/"], [:LITERAL, "page+"]]],
- ["/page,", [[:SLASH, "/"], [:LITERAL, "page,"]]],
- ["/page;", [[:SLASH, "/"], [:LITERAL, "page;"]]],
- ["/page=", [[:SLASH, "/"], [:LITERAL, "page="]]],
- ["/page@", [[:SLASH, "/"], [:LITERAL, "page@"]]],
- ['/page\:', [[:SLASH, "/"], [:LITERAL, "page:"]]],
- ['/page\(', [[:SLASH, "/"], [:LITERAL, "page("]]],
- ['/page\)', [[:SLASH, "/"], [:LITERAL, "page)"]]],
- ["/~page", [[:SLASH, "/"], [:LITERAL, "~page"]]],
- ["/pa-ge", [[:SLASH, "/"], [:LITERAL, "pa-ge"]]],
- ["/:page", [[:SLASH, "/"], [:SYMBOL, ":page"]]],
- ["/(:page)", [
+ CASES = [
+ ["/", [[:SLASH, "/"]]],
+ ["*omg", [[:STAR, "*omg"]]],
+ ["/page", [[:SLASH, "/"], [:LITERAL, "page"]]],
+ ["/page!", [[:SLASH, "/"], [:LITERAL, "page!"]]],
+ ["/page$", [[:SLASH, "/"], [:LITERAL, "page$"]]],
+ ["/page&", [[:SLASH, "/"], [:LITERAL, "page&"]]],
+ ["/page'", [[:SLASH, "/"], [:LITERAL, "page'"]]],
+ ["/page*", [[:SLASH, "/"], [:LITERAL, "page*"]]],
+ ["/page+", [[:SLASH, "/"], [:LITERAL, "page+"]]],
+ ["/page,", [[:SLASH, "/"], [:LITERAL, "page,"]]],
+ ["/page;", [[:SLASH, "/"], [:LITERAL, "page;"]]],
+ ["/page=", [[:SLASH, "/"], [:LITERAL, "page="]]],
+ ["/page@", [[:SLASH, "/"], [:LITERAL, "page@"]]],
+ ['/page\:', [[:SLASH, "/"], [:LITERAL, "page:"]]],
+ ['/page\(', [[:SLASH, "/"], [:LITERAL, "page("]]],
+ ['/page\)', [[:SLASH, "/"], [:LITERAL, "page)"]]],
+ ["/~page", [[:SLASH, "/"], [:LITERAL, "~page"]]],
+ ["/pa-ge", [[:SLASH, "/"], [:LITERAL, "pa-ge"]]],
+ ["/:page", [[:SLASH, "/"], [:SYMBOL, ":page"]]],
+ ["/(:page)", [
+ [:SLASH, "/"],
+ [:LPAREN, "("],
+ [:SYMBOL, ":page"],
+ [:RPAREN, ")"],
+ ]],
+ ["(/:action)", [
+ [:LPAREN, "("],
[:SLASH, "/"],
+ [:SYMBOL, ":action"],
+ [:RPAREN, ")"],
+ ]],
+ ["(())", [[:LPAREN, "("],
+ [:LPAREN, "("], [:RPAREN, ")"], [:RPAREN, ")"]]],
+ ["(.:format)", [
[:LPAREN, "("],
- [:SYMBOL, ":page"],
+ [:DOT, "."],
+ [:SYMBOL, ":format"],
[:RPAREN, ")"],
]],
- ["(/:action)", [
- [:LPAREN, "("],
- [:SLASH, "/"],
- [:SYMBOL, ":action"],
- [:RPAREN, ")"],
- ]],
- ["(())", [[:LPAREN, "("],
- [:LPAREN, "("], [:RPAREN, ")"], [:RPAREN, ")"]]],
- ["(.:format)", [
- [:LPAREN, "("],
- [:DOT, "."],
- [:SYMBOL, ":format"],
- [:RPAREN, ")"],
- ]],
- ].each do |str, expected|
- @scanner.scan_setup str
- assert_tokens expected, @scanner
+ ]
+
+ CASES.each do |pattern, expected_tokens|
+ test "Scanning `#{pattern}`" do
+ @scanner.scan_setup pattern
+ assert_tokens expected_tokens, @scanner, pattern
end
end
- def assert_tokens(tokens, scanner)
- toks = []
- while tok = scanner.next_token
- toks << tok
+ private
+
+ def assert_tokens(expected_tokens, scanner, pattern)
+ actual_tokens = []
+ while token = scanner.next_token
+ actual_tokens << token
+ end
+ assert_equal expected_tokens, actual_tokens, "Wrong tokens for `#{pattern}`"
end
- assert_equal tokens, toks
- end
end
end
end
diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb
index 29cc74471d..183f421bcf 100644
--- a/actionpack/test/journey/router_test.rb
+++ b/actionpack/test/journey/router_test.rb
@@ -30,7 +30,7 @@ module ActionDispatch
def test_unicode
get "/ほげ", to: "foo#bar"
- #match the escaped version of /ほげ
+ # match the escaped version of /ほげ
env = rails_env "PATH_INFO" => "/%E3%81%BB%E3%81%92"
called = false
router.recognize(env) do |r, params|
diff --git a/actionpack/test/journey/routes_test.rb b/actionpack/test/journey/routes_test.rb
index 81ce07526f..d5c81a8421 100644
--- a/actionpack/test/journey/routes_test.rb
+++ b/actionpack/test/journey/routes_test.rb
@@ -17,11 +17,11 @@ module ActionDispatch
def test_clear
mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron"
- assert_not_predicate routes, :empty?
+ assert_not_empty routes
assert_equal 1, routes.length
routes.clear
- assert routes.empty?
+ assert_empty routes
assert_equal 0, routes.length
end
@@ -43,7 +43,7 @@ module ActionDispatch
mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron"
assert_equal 1, @routes.anchored_routes.length
- assert_predicate @routes.custom_routes, :empty?
+ assert_empty @routes.custom_routes
mapper.get "/hello/:who", to: "foo#bar", as: "bar", who: /\d/
diff --git a/actionpack/test/tmp/.gitignore b/actionpack/test/tmp/.gitignore
deleted file mode 100644
index e69de29bb2..0000000000
--- a/actionpack/test/tmp/.gitignore
+++ /dev/null
diff --git a/actionview/.gitignore b/actionview/.gitignore
index 0a04b29786..246aabbb7f 100644
--- a/actionview/.gitignore
+++ b/actionview/.gitignore
@@ -1,2 +1,5 @@
-/lib/assets/compiled
-/tmp
+/lib/assets/compiled/
+/log/
+/test/fixtures/public/absolute/
+/test/ujs/log/
+/tmp/
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index b22686d1d8..2c1ca12043 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,35 +1,71 @@
-* Remove default `alt` text generation.
+* Fix JavaScript views rendering does not work with Firefox when using
+ Content Security Policy.
- Fixes #30096
+ Fixes #32577.
- *Cameron Cundiff*
+ *Yuji Yaginuma*
-* Add `srcset` option to `image_tag` helper.
+* Add the `nonce: true` option for `javascript_include_tag` helper to
+ support automatic nonce generation for Content Security Policy.
+ Works the same way as `javascript_tag nonce: true` does.
- *Roberto Miranda*
+ *Yaroslav Markin*
-* Fix issues with scopes and engine on `current_page?` method.
+* Remove `ActionView::Helpers::RecordTagHelper`.
- Fixes #29401.
+ *Yoshiyuki Hirano*
- *Nikita Savrov*
+* Disable `ActionView::Template` finalizers in test environment.
-* Generate field ids in `collection_check_boxes` and `collection_radio_buttons`.
+ Template finalization can be expensive in large view test suites.
+ Add a configuration option,
+ `action_view.finalize_compiled_template_methods`, and turn it off in
+ the test environment.
- This makes sure that the labels are linked up with the fields.
+ *Simon Coffey*
- Fixes #29014.
+* Extract the `confirm` call in its own, overridable method in `rails_ujs`.
+ Example :
+ Rails.confirm = function(message, element) {
+ return (my_bootstrap_modal_confirm(message));
+ }
- *Yuji Yaginuma*
+ *Mathieu Mahé*
+
+* Enable select tag helper to mark `prompt` option as `selected` and/or `disabled` for `required`
+ field. Example:
+
+ select :post,
+ :category,
+ ["lifestyle", "programming", "spiritual"],
+ { selected: "", disabled: "", prompt: "Choose one" },
+ { required: true }
+
+ Placeholder option would be selected and disabled. The HTML produced:
+
+ <select required="required" name="post[category]" id="post_category">
+ <option disabled="disabled" selected="selected" value="">Choose one</option>
+ <option value="lifestyle">lifestyle</option>
+ <option value="programming">programming</option>
+ <option value="spiritual">spiritual</option></select>
+
+ *Sergey Prikhodko*
+
+* Don't enforce UTF-8 by default.
+
+ With the disabling of TLS 1.0 by most major websites, continuing to run
+ IE8 or lower becomes increasingly difficult so default to not enforcing
+ UTF-8 encoding as it's not relevant to other browsers.
+
+ *Andrew White*
-* Add `:json` type to `auto_discovery_link_tag` to support [JSON Feeds](https://jsonfeed.org/version/1)
+* Change translation key of `submit_tag` from `module_name_class_name` to `module_name/class_name`.
- *Mike Gunderloy*
+ *Rui Onodera*
-* Update `distance_of_time_in_words` helper to display better error messages
- for bad input.
+* Rails 6 requires Ruby 2.4.1 or newer.
- *Jay Hayes*
+ *Jeremy Daer*
-Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionview/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actionview/CHANGELOG.md) for previous changes.
diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE
index ac810e86d0..1cb3add0fc 100644
--- a/actionview/MIT-LICENSE
+++ b/actionview/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2017 David Heinemeier Hansson
+Copyright (c) 2004-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionview/README.rdoc b/actionview/README.rdoc
index d5029599b7..03a0723564 100644
--- a/actionview/README.rdoc
+++ b/actionview/README.rdoc
@@ -11,7 +11,7 @@ The latest version of Action View can be installed with RubyGems:
$ gem install actionview
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/actionview
@@ -29,7 +29,7 @@ API documentation is at
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/actionview/RUNNING_UJS_TESTS.rdoc b/actionview/RUNNING_UJS_TESTS.rdoc
index a575624a06..e30c2aee55 100644
--- a/actionview/RUNNING_UJS_TESTS.rdoc
+++ b/actionview/RUNNING_UJS_TESTS.rdoc
@@ -1,7 +1,8 @@
== Running UJS tests
-Ensure that you can build the project and run tests.
-Run rake ujs:server first, and then run the web tests by
-visiting http://localhost:4567 in your browser.
+Ensure that you can build the project by running:
+ rake ujs:server
-rake ujs:server
+Then run the web tests by visiting the following URL in your browser:
+
+ http://localhost:4567
diff --git a/actionview/RUNNING_UNIT_TESTS.rdoc b/actionview/RUNNING_UNIT_TESTS.rdoc
index 6c4e5e983a..4442dbdb9e 100644
--- a/actionview/RUNNING_UNIT_TESTS.rdoc
+++ b/actionview/RUNNING_UNIT_TESTS.rdoc
@@ -2,13 +2,13 @@
The easiest way to run the unit tests is through Rake. The default task runs
the entire test suite for all classes. For more information, checkout the
-full array of rake tasks with "rake -T"
+full array of rake tasks with <tt>rake -T</tt>
-Rake can be found at http://docs.seattlerb.org/rake/.
+Rake can be found at https://ruby.github.io/rake/.
== Running by hand
-To run a single test suite
+Run a single test suite:
rake test TEST=path/to/test.rb
@@ -18,10 +18,9 @@ which can be further narrowed down to one test:
== Dependency on Active Record and database setup
-Test cases in the test/activerecord/ directory depend on having
-activerecord and sqlite3 installed. If Active Record is not in
-actionview/../activerecord directory, or the sqlite3 rubygem is not installed,
-these tests are skipped.
-
+Test cases in the +test/activerecord/+ directory depend on having
+activerecord+ and +sqlite3+ installed. If Active Record is not in
+actionview/../activerecord+ directory, or the +sqlite3+ Ruby gem is not installed,
+ these tests are skipped.
Other tests are runnable from a fresh copy of actionview without any configuration.
diff --git a/actionview/Rakefile b/actionview/Rakefile
index 20dfa4e114..bdfd96c141 100644
--- a/actionview/Rakefile
+++ b/actionview/Rakefile
@@ -29,10 +29,11 @@ namespace :test do
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
+ desc "Run tests for rails-ujs"
task :ujs do
begin
Dir.mkdir("log")
- pid = spawn("bundle exec rackup test/ujs/config.ru -p 4567 -s puma > log/test.log 2>&1")
+ pid = spawn("bundle exec rackup test/ujs/config.ru -p 4567 -s puma > log/test.log 2>&1", pgroup: true)
start_time = Time.now
@@ -45,10 +46,10 @@ namespace :test do
end
end
- system("npm run lint && phantomjs ../ci/phantomjs.js http://localhost:4567/")
- status = $?.to_i
+ system("npm run lint && bundle exec ruby ../ci/qunit-selenium-runner.rb http://localhost:4567/")
+ status = $?.exitstatus
ensure
- Process.kill("KILL", pid) if pid
+ Process.kill("KILL", -pid) if pid
FileUtils.rm_rf("log")
end
@@ -56,7 +57,7 @@ namespace :test do
end
namespace :integration do
- desc "ActiveRecord Integration Tests"
+ # Active Record Integration Tests
Rake::TestTask.new(:active_record) do |t|
t.libs << "test"
t.test_files = Dir.glob("test/activerecord/*_test.rb")
@@ -65,7 +66,7 @@ namespace :test do
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
- desc "ActionPack Integration Tests"
+ # Action Pack Integration Tests
Rake::TestTask.new(:action_pack) do |t|
t.libs << "test"
t.test_files = Dir.glob("test/actionpack/**/*_test.rb")
@@ -130,7 +131,7 @@ namespace :assets do
end
task :lines do
- load File.join(File.expand_path("..", __dir__), "/tools/line_statistics")
+ load File.expand_path("../tools/line_statistics", __dir__)
files = FileList["lib/**/*.rb"]
CodeTools::LineStatistics.new(files).print_loc
end
diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec
index b99137fcf6..49ee1a292b 100644
--- a/actionview/actionview.gemspec
+++ b/actionview/actionview.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Rendering framework putting the V in MVC (part of Rails)."
s.description = "Simple, battle-tested conventions and helpers for building web pages."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/actionview/app/assets/javascripts/MIT-LICENSE b/actionview/app/assets/javascripts/MIT-LICENSE
index befcbdc7b7..28e1b12496 100644
--- a/actionview/app/assets/javascripts/MIT-LICENSE
+++ b/actionview/app/assets/javascripts/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2007-2017 Rails Core team
+Copyright (c) 2007-2018 Rails Core team
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionview/app/assets/javascripts/README.md b/actionview/app/assets/javascripts/README.md
index f321b9f720..b74fa1afad 100644
--- a/actionview/app/assets/javascripts/README.md
+++ b/actionview/app/assets/javascripts/README.md
@@ -1,5 +1,4 @@
-Ruby on Rails unobtrusive scripting adapter.
-========================================
+# Ruby on Rails unobtrusive scripting adapter
This unobtrusive scripting support file is developed for the Ruby on Rails framework, but is not strictly tied to any specific backend. You can drop this into any application to:
@@ -8,53 +7,51 @@ This unobtrusive scripting support file is developed for the Ruby on Rails frame
- make forms or hyperlinks submit data asynchronously with Ajax;
- have submit buttons become automatically disabled on form submit to prevent double-clicking.
-These features are achieved by adding certain ["data" attributes][data] to your HTML markup. In Rails, they are added by the framework's template helpers.
+These features are achieved by adding certain [`data` attributes][data] to your HTML markup. In Rails, they are added by the framework's template helpers.
-Requirements
-------------
+## Optional prerequisites
-- HTML5 doctype (optional).
+Note that the `data` attributes this library adds are a feature of HTML5. If you're not targeting HTML5, these attributes may make your HTML to fail [validation][validator]. However, this shouldn't create any issues for web browsers or other user agents.
-If you don't use HTML5, adding "data" attributes to your HTML4 or XHTML pages might make them fail [W3C markup validation][validator]. However, this shouldn't create any issues for web browsers or other user agents.
+## Installation
-Installation using npm
-------------
+### NPM
-Run `npm install rails-ujs --save` to install the rails-ujs package.
+ npm install rails-ujs --save
+
+### Yarn
+
+ yarn add rails-ujs
-Installation using Yarn
-------------
+Ensure that `.yarnclean` does not include `assets` if you use [yarn autoclean](https://yarnpkg.com/lang/en/docs/cli/autoclean/).
-Run `yarn add rails-ujs` to install the rails-ujs package.
+## Usage
-Usage
-------------
+### Asset pipeline
-Require `rails-ujs` in your application.js manifest.
+In a conventional Rails application that uses the asset pipeline, require `rails-ujs` in your `application.js` manifest:
```javascript
//= require rails-ujs
```
-Usage with yarn
-------------
+### ES2015+
-When using with the Webpacker gem or your preferred JavaScript bundler, just
-add the following to your main JS file and compile.
+If you're using the Webpacker gem or some other JavaScript bundler, add the following to your main JS file:
```javascript
import Rails from 'rails-ujs';
Rails.start()
```
-How to run tests
-------------
+## How to run tests
Run `bundle exec rake ujs:server` first, and then run the web tests by visiting http://localhost:4567 in your browser.
## License
+
rails-ujs is released under the [MIT License](MIT-LICENSE).
-[data]: http://www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes "Embedding custom non-visible data with the data-* attributes"
+[data]: https://www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-attributes "Embedding custom non-visible data with the data-* attributes"
[validator]: http://validator.w3.org/
[csrf]: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html
diff --git a/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee b/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee
index 72b5aaa218..0738ffcdc9 100644
--- a/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee
+++ b/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee
@@ -5,6 +5,10 @@
Rails.handleConfirm = (e) ->
stopEverything(e) unless allowAction(this)
+# Default confirm dialog, may be overridden with custom confirm dialog in Rails.confirm
+Rails.confirm = (message, element) ->
+ confirm(message)
+
# For 'data-confirm' attribute:
# - Fires `confirm` event
# - Shows the confirmation dialog
@@ -20,7 +24,7 @@ allowAction = (element) ->
answer = false
if fire(element, 'confirm')
- try answer = confirm(message)
+ try answer = Rails.confirm(message, element)
callback = fire(element, 'confirm:complete', [answer])
answer and callback
diff --git a/actionview/app/assets/javascripts/rails-ujs/features/remote.coffee b/actionview/app/assets/javascripts/rails-ujs/features/remote.coffee
index 852587042c..b3448dabac 100644
--- a/actionview/app/assets/javascripts/rails-ujs/features/remote.coffee
+++ b/actionview/app/assets/javascripts/rails-ujs/features/remote.coffee
@@ -62,7 +62,7 @@ Rails.handleRemote = (e) ->
fire(element, 'ajax:send', [xhr])
else
fire(element, 'ajax:stopped')
- xhr.abort()
+ return false
success: (args...) -> fire(element, 'ajax:success', args)
error: (args...) -> fire(element, 'ajax:error', args)
complete: (args...) -> fire(element, 'ajax:complete', args)
diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee
index a653d3af3d..019bda635a 100644
--- a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee
@@ -1,7 +1,8 @@
+#= require ./csp
#= require ./csrf
#= require ./event
-{ CSRFProtection, fire } = Rails
+{ cspNonce, CSRFProtection, fire } = Rails
AcceptHeaders =
'*': '*/*'
@@ -20,13 +21,12 @@ Rails.ajax = (options) ->
else
options.error?(response, xhr.statusText, xhr)
options.complete?(xhr, xhr.statusText)
- # Call beforeSend hook
- options.beforeSend?(xhr, options)
- # Send the request
+
+ if options.beforeSend? && !options.beforeSend(xhr, options)
+ return false
+
if xhr.readyState is XMLHttpRequest.OPENED
xhr.send(options.data)
- else
- fire(document, 'ajaxStop') # to be compatible with jQuery.ajax
prepareOptions = (options) ->
options.url = options.url or location.href
@@ -66,9 +66,10 @@ processResponse = (response, type) ->
try response = JSON.parse(response)
else if type.match(/\b(?:java|ecma)script\b/)
script = document.createElement('script')
+ script.setAttribute('nonce', cspNonce())
script.text = response
document.head.appendChild(script).parentNode.removeChild(script)
- else if type.match(/\b(xml|html|svg)\b/)
+ else if type.match(/\bxml\b/)
parser = new DOMParser()
type = type.replace(/;.+/, '') # remove something like ';charset=utf-8'
try response = parser.parseFromString(response, type)
diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee
new file mode 100644
index 0000000000..8d2d6ce447
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee
@@ -0,0 +1,4 @@
+# Content-Security-Policy nonce for inline scripts
+cspNonce = Rails.cspNonce = ->
+ meta = document.querySelector('meta[name=csp-nonce]')
+ meta and meta.content
diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/dom.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/dom.coffee
index 6bef618147..3d3c5bb330 100644
--- a/actionview/app/assets/javascripts/rails-ujs/utils/dom.coffee
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/dom.coffee
@@ -5,6 +5,13 @@ m = Element.prototype.matches or
Element.prototype.oMatchesSelector or
Element.prototype.webkitMatchesSelector
+# Checks if the given native dom element matches the selector
+# element::
+# native DOM element
+# selector::
+# css selector string or
+# a javascript object with `selector` and `exclude` properties
+# Examples: "form", { selector: "form", exclude: "form[data-remote='true']"}
Rails.matches = (element, selector) ->
if selector.exclude?
m.call(element, selector.selector) and not m.call(element, selector.exclude)
diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee
index 8d3ff007ea..a7eee52060 100644
--- a/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee
@@ -11,9 +11,26 @@ if typeof CustomEvent isnt 'function'
evt = document.createEvent('CustomEvent')
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail)
evt
+
CustomEvent.prototype = window.Event.prototype
+ # Fix setting `defaultPrevented` when `preventDefault()` is called
+ # http://stackoverflow.com/questions/23349191/event-preventdefault-is-not-working-in-ie-11-for-custom-events
+ { preventDefault } = CustomEvent.prototype
+ CustomEvent.prototype.preventDefault = ->
+ result = preventDefault.call(this)
+ if @cancelable and not @defaultPrevented
+ Object.defineProperty(this, 'defaultPrevented', get: -> true)
+ result
+
# Triggers a custom event on an element and returns false if the event result is false
+# obj::
+# a native DOM element
+# name::
+# string that corrspends to the event you want to trigger
+# e.g. 'click', 'submit'
+# data::
+# data you want to pass when you dispatch an event
fire = Rails.fire = (obj, name, data) ->
event = new CustomEvent(
name,
@@ -31,6 +48,17 @@ Rails.stopEverything = (e) ->
e.stopPropagation()
e.stopImmediatePropagation()
+# Delegates events
+# to a specified parent `element`, which fires event `handler`
+# for the specified `selector` when an event of `eventType` is triggered
+# element::
+# parent element that will listen for events e.g. document
+# selector::
+# css selector; or an object that has `selector` and `exclude` properties (see: Rails.matches)
+# eventType::
+# string representing the event e.g. 'submit', 'click'
+# handler::
+# the event handler to be called
Rails.delegate = (element, selector, eventType, handler) ->
element.addEventListener eventType, (e) ->
target = e.target
diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee
index 5fa337b518..736cab08db 100644
--- a/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee
@@ -10,7 +10,7 @@ Rails.serializeElement = (element, additionalParam) ->
params = []
inputs.forEach (input) ->
- return unless input.name
+ return if !input.name || input.disabled
if matches(input, 'select')
toArray(input.options).forEach (option) ->
params.push(name: input.name, value: option.value) if option.selected
diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb
index 3c8a8488a5..c1eeda75f5 100644
--- a/actionview/lib/action_view.rb
+++ b/actionview/lib/action_view.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2004-2017 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -25,7 +25,7 @@
require "active_support"
require "active_support/rails"
-require_relative "action_view/version"
+require "action_view/version"
module ActionView
extend ActiveSupport::Autoload
@@ -76,7 +76,6 @@ module ActionView
autoload :MissingTemplate
autoload :ActionViewError
autoload :EncodingError
- autoload :MissingRequestError
autoload :TemplateError
autoload :WrongEncodingError
end
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
index 637c8e7708..d41fe2a608 100644
--- a/actionview/lib/action_view/base.rb
+++ b/actionview/lib/action_view/base.rb
@@ -3,11 +3,11 @@
require "active_support/core_ext/module/attr_internal"
require "active_support/core_ext/module/attribute_accessors"
require "active_support/ordered_options"
-require_relative "log_subscriber"
-require_relative "helpers"
-require_relative "context"
-require_relative "template"
-require_relative "lookup_context"
+require "action_view/log_subscriber"
+require "action_view/helpers"
+require "action_view/context"
+require "action_view/template"
+require "action_view/lookup_context"
module ActionView #:nodoc:
# = Action View Base
diff --git a/actionview/lib/action_view/context.rb b/actionview/lib/action_view/context.rb
index e1b02fbde4..3c605c3ee3 100644
--- a/actionview/lib/action_view/context.rb
+++ b/actionview/lib/action_view/context.rb
@@ -10,16 +10,16 @@ module ActionView
# Action View contexts are supplied to Action Controller to render a template.
# The default Action View context is ActionView::Base.
#
- # In order to work with ActionController, a Context must just include this module.
- # The initialization of the variables used by the context (@output_buffer, @view_flow,
- # and @virtual_path) is responsibility of the object that includes this module
- # (although you can call _prepare_context defined below).
+ # In order to work with Action Controller, a Context must just include this
+ # module. The initialization of the variables used by the context
+ # (@output_buffer, @view_flow, and @virtual_path) is responsibility of the
+ # object that includes this module (although you can call _prepare_context
+ # defined below).
module Context
include CompiledTemplates
attr_accessor :output_buffer, :view_flow
# Prepares the context by setting the appropriate instance variables.
- # :api: plugin
def _prepare_context
@view_flow = OutputFlow.new
@output_buffer = nil
@@ -29,7 +29,6 @@ module ActionView
# Encapsulates the interaction with the view flow so it
# returns the correct buffer on +yield+. This is usually
# overwritten by helpers to add more behavior.
- # :api: plugin
def _layout_for(name = nil)
name ||= :layout
view_flow.get(name).html_safe
diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb
index 02bd0545c2..182f6e2eef 100644
--- a/actionview/lib/action_view/dependency_tracker.rb
+++ b/actionview/lib/action_view/dependency_tracker.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "concurrent/map"
-require_relative "path_set"
+require "action_view/path_set"
module ActionView
class DependencyTracker # :nodoc:
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index e404ebb6b6..3832293251 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "concurrent/map"
-require_relative "dependency_tracker"
+require "action_view/dependency_tracker"
require "monitor"
module ActionView
@@ -46,10 +46,7 @@ module ActionView
def tree(name, finder, partial = false, seen = {})
logical_name = name.gsub(%r|/_|, "/")
- options = {}
- options[:formats] = [finder.rendered_format] if finder.rendered_format
-
- if template = finder.disable_cache { finder.find_all(logical_name, [], partial, [], options).first }
+ if template = find_template(finder, logical_name, [], partial, [])
finder.rendered_format ||= template.formats.first
if node = seen[template.identifier] # handle cycles in the tree
@@ -71,6 +68,22 @@ module ActionView
seen[name] ||= Missing.new(name, logical_name, nil)
end
end
+
+ private
+ def find_template(finder, *args)
+ name = args.first
+ prefixes = args[1] || []
+ partial = args[2] || false
+ keys = args[3] || []
+ options = args[4] || {}
+ finder.disable_cache do
+ if format = finder.rendered_format
+ finder.find_all(name, prefixes, partial, keys, options.merge(formats: [format])).first || finder.find_all(name, prefixes, partial, keys, options).first
+ else
+ finder.find_all(name, prefixes, partial, keys, options).first
+ end
+ end
+ end
end
class Node
@@ -89,7 +102,7 @@ module ActionView
end
def digest(finder, stack = [])
- Digest::MD5.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}")
+ ActiveSupport::Digest.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}")
end
def dependency_digest(finder, stack)
diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb
index ed92490be7..77ae444a58 100644
--- a/actionview/lib/action_view/gem_version.rb
+++ b/actionview/lib/action_view/gem_version.rb
@@ -7,8 +7,8 @@ module ActionView
end
module VERSION
- MAJOR = 5
- MINOR = 2
+ MAJOR = 6
+ MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb
index 46f20c4277..0d77f74171 100644
--- a/actionview/lib/action_view/helpers.rb
+++ b/actionview/lib/action_view/helpers.rb
@@ -13,6 +13,7 @@ module ActionView #:nodoc:
autoload :CacheHelper
autoload :CaptureHelper
autoload :ControllerHelper
+ autoload :CspHelper
autoload :CsrfHelper
autoload :DateHelper
autoload :DebugHelper
@@ -22,7 +23,6 @@ module ActionView #:nodoc:
autoload :JavaScriptHelper, "action_view/helpers/javascript_helper"
autoload :NumberHelper
autoload :OutputSafetyHelper
- autoload :RecordTagHelper
autoload :RenderingHelper
autoload :SanitizeHelper
autoload :TagHelper
@@ -46,6 +46,7 @@ module ActionView #:nodoc:
include CacheHelper
include CaptureHelper
include ControllerHelper
+ include CspHelper
include CsrfHelper
include DateHelper
include DebugHelper
@@ -55,7 +56,6 @@ module ActionView #:nodoc:
include JavaScriptHelper
include NumberHelper
include OutputSafetyHelper
- include RecordTagHelper
include RenderingHelper
include SanitizeHelper
include TagHelper
diff --git a/actionview/lib/action_view/helpers/active_model_helper.rb b/actionview/lib/action_view/helpers/active_model_helper.rb
index f1ef715710..e41a95d2ce 100644
--- a/actionview/lib/action_view/helpers/active_model_helper.rb
+++ b/actionview/lib/action_view/helpers/active_model_helper.rb
@@ -17,8 +17,8 @@ module ActionView
end
end
- def content_tag(*)
- error_wrapping(super)
+ def content_tag(type, options, *)
+ select_markup_helper?(type) ? super : error_wrapping(super)
end
def tag(type, options, *)
@@ -43,6 +43,10 @@ module ActionView
object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present?
end
+ def select_markup_helper?(type)
+ ["optgroup", "option"].include?(type)
+ end
+
def tag_generate_errors?(options)
options["type"] != "hidden"
end
diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb
index bc2713d13e..14bd8ffa84 100644
--- a/actionview/lib/action_view/helpers/asset_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb
@@ -2,8 +2,10 @@
require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/hash/keys"
-require_relative "asset_url_helper"
-require_relative "tag_helper"
+require "active_support/core_ext/object/inclusion"
+require "active_support/core_ext/object/try"
+require "action_view/helpers/asset_url_helper"
+require "action_view/helpers/tag_helper"
module ActionView
# = Action View Asset Tag Helpers
@@ -37,19 +39,24 @@ module ActionView
# When the Asset Pipeline is enabled, you can pass the name of your manifest as
# source, and include other JavaScript or CoffeeScript files inside the manifest.
#
+ # If the server supports Early Hints header links for these assets will be
+ # automatically pushed.
+ #
# ==== Options
#
# When the last parameter is a hash you can add HTML attributes using that
# parameter. The following options are supported:
#
- # * <tt>:extname</tt> - Append an extension to the generated url unless the extension
- # already exists. This only applies for relative urls.
- # * <tt>:protocol</tt> - Sets the protocol of the generated url, this option only
- # applies when a relative url and +host+ options are provided.
- # * <tt>:host</tt> - When a relative url is provided the host is added to the
+ # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension
+ # already exists. This only applies for relative URLs.
+ # * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only
+ # applies when a relative URL and +host+ options are provided.
+ # * <tt>:host</tt> - When a relative URL is provided the host is added to the
# that path.
# * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
# when it is set to true.
+ # * <tt>:nonce<tt> - When set to true, adds an automatic nonce value if
+ # you have Content Security Policy enabled.
#
# ==== Examples
#
@@ -74,15 +81,29 @@ module ActionView
#
# javascript_include_tag "http://www.example.com/xmlhr.js"
# # => <script src="http://www.example.com/xmlhr.js"></script>
+ #
+ # javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true
+ # # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script>
def javascript_include_tag(*sources)
options = sources.extract_options!.stringify_keys
path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
- sources.uniq.map { |source|
+ early_hints_links = []
+
+ sources_tags = sources.uniq.map { |source|
+ href = path_to_javascript(source, path_options)
+ early_hints_links << "<#{href}>; rel=preload; as=script"
tag_options = {
- "src" => path_to_javascript(source, path_options)
+ "src" => href
}.merge!(options)
+ if tag_options["nonce"] == true
+ tag_options["nonce"] = content_security_policy_nonce
+ end
content_tag("script".freeze, "", tag_options)
}.join("\n").html_safe
+
+ request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request
+
+ sources_tags
end
# Returns a stylesheet link tag for the sources specified as arguments. If
@@ -92,6 +113,9 @@ module ActionView
# to "screen", so you must explicitly set it to "all" for the stylesheet(s) to
# apply to all media types.
#
+ # If the server supports Early Hints header links for these assets will be
+ # automatically pushed.
+ #
# stylesheet_link_tag "style"
# # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
#
@@ -113,14 +137,22 @@ module ActionView
def stylesheet_link_tag(*sources)
options = sources.extract_options!.stringify_keys
path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys
- sources.uniq.map { |source|
+ early_hints_links = []
+
+ sources_tags = sources.uniq.map { |source|
+ href = path_to_stylesheet(source, path_options)
+ early_hints_links << "<#{href}>; rel=preload; as=style"
tag_options = {
"rel" => "stylesheet",
"media" => "screen",
- "href" => path_to_stylesheet(source, path_options)
+ "href" => href
}.merge!(options)
tag(:link, tag_options)
}.join("\n").html_safe
+
+ request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request
+
+ sources_tags
end
# Returns a link tag that browsers and feed readers can use to auto-detect
@@ -199,8 +231,69 @@ module ActionView
}.merge!(options.symbolize_keys))
end
+ # Returns a link tag that browsers can use to preload the +source+.
+ # The +source+ can be the path of a resource managed by asset pipeline,
+ # a full path, or an URI.
+ #
+ # ==== Options
+ #
+ # * <tt>:type</tt> - Override the auto-generated mime type, defaults to the mime type for +source+ extension.
+ # * <tt>:as</tt> - Override the auto-generated value for as attribute, calculated using +source+ extension and mime type.
+ # * <tt>:crossorigin</tt> - Specify the crossorigin attribute, required to load cross-origin resources.
+ # * <tt>:nopush</tt> - Specify if the use of server push is not desired for the resource. Defaults to +false+.
+ #
+ # ==== Examples
+ #
+ # preload_link_tag("custom_theme.css")
+ # # => <link rel="preload" href="/assets/custom_theme.css" as="style" type="text/css" />
+ #
+ # preload_link_tag("/videos/video.webm")
+ # # => <link rel="preload" href="/videos/video.mp4" as="video" type="video/webm" />
+ #
+ # preload_link_tag(post_path(format: :json), as: "fetch")
+ # # => <link rel="preload" href="/posts.json" as="fetch" type="application/json" />
+ #
+ # preload_link_tag("worker.js", as: "worker")
+ # # => <link rel="preload" href="/assets/worker.js" as="worker" type="text/javascript" />
+ #
+ # preload_link_tag("//example.com/font.woff2")
+ # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>
+ #
+ # preload_link_tag("//example.com/font.woff2", crossorigin: "use-credentials")
+ # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" />
+ #
+ # preload_link_tag("/media/audio.ogg", nopush: true)
+ # # => <link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" />
+ #
+ def preload_link_tag(source, options = {})
+ href = asset_path(source, skip_pipeline: options.delete(:skip_pipeline))
+ extname = File.extname(source).downcase.delete(".")
+ mime_type = options.delete(:type) || Template::Types[extname].try(:to_s)
+ as_type = options.delete(:as) || resolve_link_as(extname, mime_type)
+ crossorigin = options.delete(:crossorigin)
+ crossorigin = "anonymous" if crossorigin == true || (crossorigin.blank? && as_type == "font")
+ nopush = options.delete(:nopush) || false
+
+ link_tag = tag.link({
+ rel: "preload",
+ href: href,
+ as: as_type,
+ type: mime_type,
+ crossorigin: crossorigin
+ }.merge!(options.symbolize_keys))
+
+ early_hints_link = "<#{href}>; rel=preload; as=#{as_type}"
+ early_hints_link += "; type=#{mime_type}" if mime_type
+ early_hints_link += "; crossorigin=#{crossorigin}" if crossorigin
+ early_hints_link += "; nopush" if nopush
+
+ request.send_early_hints("Link" => early_hints_link) if respond_to?(:request) && request
+
+ link_tag
+ end
+
# Returns an HTML image tag for the +source+. The +source+ can be a full
- # path, a file or an Active Storage attachment.
+ # path, a file, or an Active Storage attachment.
#
# ==== Options
#
@@ -240,9 +333,9 @@ module ActionView
#
# image_tag(user.avatar)
# # => <img src="/rails/active_storage/blobs/.../tiger.jpg" />
- # image_tag(user.avatar.variant(resize: "100x100"))
+ # image_tag(user.avatar.variant(resize_to_fit: [100, 100]))
# # => <img src="/rails/active_storage/variants/.../tiger.jpg" />
- # image_tag(user.avatar.variant(resize: "100x100"), size: '100')
+ # image_tag(user.avatar.variant(resize_to_fit: [100, 100]), size: '100')
# # => <img width="100" height="100" src="/rails/active_storage/variants/.../tiger.jpg" />
def image_tag(source, options = {})
options = options.symbolize_keys
@@ -288,12 +381,13 @@ module ActionView
# Returns an HTML video tag for the +sources+. If +sources+ is a string,
# a single video tag will be returned. If +sources+ is an array, a video
# tag with nested source tags for each source will be returned. The
- # +sources+ can be full paths or files that exists in your public videos
+ # +sources+ can be full paths or files that exist in your public videos
# directory.
#
# ==== Options
- # You can add HTML attributes using the +options+. The +options+ supports
- # two additional keys for convenience and conformance:
+ #
+ # When the last parameter is a hash you can add HTML attributes using that
+ # parameter. The following options are supported:
#
# * <tt>:poster</tt> - Set an image (like a screenshot) to be shown
# before the video loads. The path is calculated like the +src+ of +image_tag+.
@@ -310,7 +404,7 @@ module ActionView
# video_tag("trailer.ogg")
# # => <video src="/videos/trailer.ogg"></video>
# video_tag("trailer.ogg", controls: true, preload: 'none')
- # # => <video preload="none" controls="controls" src="/videos/trailer.ogg" ></video>
+ # # => <video preload="none" controls="controls" src="/videos/trailer.ogg"></video>
# video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png")
# # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video>
# video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png", poster_skip_pipeline: true)
@@ -337,9 +431,14 @@ module ActionView
end
end
- # Returns an HTML audio tag for the +source+.
- # The +source+ can be full path or file that exists in
- # your public audios directory.
+ # Returns an HTML audio tag for the +sources+. If +sources+ is a string,
+ # a single audio tag will be returned. If +sources+ is an array, an audio
+ # tag with nested source tags for each source will be returned. The
+ # +sources+ can be full paths or files that exist in your public audios
+ # directory.
+ #
+ # When the last parameter is a hash you can add HTML attributes using that
+ # parameter.
#
# audio_tag("sound")
# # => <audio src="/audios/sound"></audio>
@@ -395,6 +494,18 @@ module ActionView
raise ArgumentError, "Cannot pass a :size option with a :height or :width option"
end
end
+
+ def resolve_link_as(extname, mime_type)
+ if extname == "js"
+ "script"
+ elsif extname == "css"
+ "style"
+ elsif extname == "vtt"
+ "track"
+ elsif (type = mime_type.to_s.split("/")[0]) && type.in?(%w(audio video font))
+ type
+ end
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb
index a4dcfc9a6c..8cbe107e41 100644
--- a/actionview/lib/action_view/helpers/asset_url_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_url_helper.rb
@@ -6,7 +6,7 @@ module ActionView
# = Action View Asset URL Helpers
module Helpers #:nodoc:
# This module provides methods for generating asset paths and
- # urls.
+ # URLs.
#
# image_path("rails.png")
# # => "/assets/rails.png"
@@ -57,8 +57,8 @@ module ActionView
# You can read more about setting up your DNS CNAME records from your ISP.
#
# Note: This is purely a browser performance optimization and is not meant
- # for server load balancing. See http://www.die.net/musings/page_load_time/
- # for background and http://www.browserscope.org/?category=network for
+ # for server load balancing. See https://www.die.net/musings/page_load_time/
+ # for background and https://www.browserscope.org/?category=network for
# connection limit data.
#
# Alternatively, you can exert more control over the asset host by setting
@@ -97,7 +97,7 @@ module ActionView
# still sending assets for plain HTTP requests from asset hosts. If you don't
# have SSL certificates for each of the asset hosts this technique allows you
# to avoid warnings in the client about mixed media.
- # Note that the request parameter might not be supplied, e.g. when the assets
+ # Note that the +request+ parameter might not be supplied, e.g. when the assets
# are precompiled via a Rake task. Make sure to use a +Proc+ instead of a lambda,
# since a +Proc+ allows missing parameters and sets them to +nil+.
#
@@ -149,13 +149,13 @@ module ActionView
# Below lists scenarios that apply to +asset_path+ whether or not you're
# using the asset pipeline.
#
- # - All fully qualified urls are returned immediately. This bypasses the
+ # - All fully qualified URLs are returned immediately. This bypasses the
# asset pipeline and all other behavior described.
#
# asset_path("http://www.example.com/js/xmlhr.js") # => "http://www.example.com/js/xmlhr.js"
#
# - All assets that begin with a forward slash are assumed to be full
- # urls and will not be expanded. This will bypass the asset pipeline.
+ # URLs and will not be expanded. This will bypass the asset pipeline.
#
# asset_path("/foo.png") # => "/foo.png"
#
@@ -324,7 +324,7 @@ module ActionView
# Since +javascript_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
- # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/dir/xmlhr.js
+ # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/js/xmlhr.js
#
def javascript_url(source, options = {})
url_to_asset(source, { type: :javascript }.merge!(options))
@@ -351,7 +351,7 @@ module ActionView
# Since +stylesheet_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
- # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/css/style.css
+ # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/assets/css/style.css
#
def stylesheet_url(source, options = {})
url_to_asset(source, { type: :stylesheet }.merge!(options))
@@ -381,7 +381,7 @@ module ActionView
# Since +image_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
- # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/edit.png
+ # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/assets/edit.png
#
def image_url(source, options = {})
url_to_asset(source, { type: :image }.merge!(options))
@@ -407,7 +407,7 @@ module ActionView
# Since +video_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
- # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/hd.avi
+ # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/videos/hd.avi
#
def video_url(source, options = {})
url_to_asset(source, { type: :video }.merge!(options))
@@ -433,7 +433,7 @@ module ActionView
# Since +audio_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
- # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/horse.wav
+ # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/audios/horse.wav
#
def audio_url(source, options = {})
url_to_asset(source, { type: :audio }.merge!(options))
@@ -458,7 +458,7 @@ module ActionView
# Since +font_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
- # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/font.ttf
+ # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/fonts/font.ttf
#
def font_url(source, options = {})
url_to_asset(source, { type: :font }.merge!(options))
diff --git a/actionview/lib/action_view/helpers/csp_helper.rb b/actionview/lib/action_view/helpers/csp_helper.rb
new file mode 100644
index 0000000000..e2e065c218
--- /dev/null
+++ b/actionview/lib/action_view/helpers/csp_helper.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module ActionView
+ # = Action View CSP Helper
+ module Helpers #:nodoc:
+ module CspHelper
+ # Returns a meta tag "csp-nonce" with the per-session nonce value
+ # for allowing inline <script> tags.
+ #
+ # <head>
+ # <%= csp_meta_tag %>
+ # </head>
+ #
+ # This is used by the Rails UJS helper to create dynamically
+ # loaded inline <script> elements.
+ #
+ def csp_meta_tag
+ if content_security_policy?
+ tag("meta", name: "csp-nonce", content: content_security_policy_nonce)
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb
index 642bd0fec6..620e1e9f21 100644
--- a/actionview/lib/action_view/helpers/date_helper.rb
+++ b/actionview/lib/action_view/helpers/date_helper.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "date"
-require_relative "tag_helper"
+require "action_view/helpers/tag_helper"
require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/date/conversions"
require "active_support/core_ext/hash/slice"
@@ -116,7 +116,7 @@ module ActionView
when 10..19 then locale.t :less_than_x_seconds, count: 20
when 20..39 then locale.t :half_a_minute
when 40..59 then locale.t :less_than_x_minutes, count: 1
- else locale.t :x_minutes, count: 1
+ else locale.t :x_minutes, count: 1
end
when 2...45 then locale.t :x_minutes, count: distance_in_minutes
@@ -131,7 +131,7 @@ module ActionView
when 43200...86400 then locale.t :about_x_months, count: (distance_in_minutes.to_f / 43200.0).round
# 60 days up to 365 days
when 86400...525600 then locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round
- else
+ else
from_year = from_time.year
from_year += 1 if from_time.month >= 3
to_year = to_time.year
@@ -302,15 +302,15 @@ module ActionView
# time_select("article", "start_time", include_seconds: true)
#
# # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30, and 45.
- # time_select 'game', 'game_time', {minute_step: 15}
+ # time_select 'game', 'game_time', { minute_step: 15 }
#
# # Creates a time select tag with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
- # time_select("article", "written_on", prompt: {hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds'})
- # time_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours
+ # time_select("article", "written_on", prompt: { hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds' })
+ # time_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours
# time_select("article", "written_on", prompt: true) # generic prompts for all
#
# # You can set :ampm option to true which will show the hours as: 12 PM, 01 AM .. 11 PM.
- # time_select 'game', 'game_time', {ampm: true}
+ # time_select 'game', 'game_time', { ampm: true }
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
#
@@ -346,8 +346,8 @@ module ActionView
# datetime_select("article", "written_on", discard_type: true)
#
# # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
- # datetime_select("article", "written_on", prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'})
- # datetime_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours
+ # datetime_select("article", "written_on", prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
+ # datetime_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours
# datetime_select("article", "written_on", prompt: true) # generic prompts for all
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
@@ -397,8 +397,8 @@ module ActionView
# select_datetime(my_date_time, prefix: 'payday')
#
# # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
- # select_datetime(my_date_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'})
- # select_datetime(my_date_time, prompt: {hour: true}) # generic prompt for hours
+ # select_datetime(my_date_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
+ # select_datetime(my_date_time, prompt: { hour: true }) # generic prompt for hours
# select_datetime(my_date_time, prompt: true) # generic prompts for all
def select_datetime(datetime = Time.current, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_datetime
@@ -436,8 +436,8 @@ module ActionView
# select_date(my_date, prefix: 'payday')
#
# # Generates a date select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
- # select_date(my_date, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'})
- # select_date(my_date, prompt: {hour: true}) # generic prompt for hours
+ # select_date(my_date, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
+ # select_date(my_date, prompt: { hour: true }) # generic prompt for hours
# select_date(my_date, prompt: true) # generic prompts for all
def select_date(date = Date.current, options = {}, html_options = {})
DateTimeSelector.new(date, options, html_options).select_date
@@ -476,8 +476,8 @@ module ActionView
# select_time(my_time, start_hour: 2, end_hour: 14)
#
# # Generates a time select with a custom prompt. Use <tt>:prompt</tt> to true for generic prompts.
- # select_time(my_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'})
- # select_time(my_time, prompt: {hour: true}) # generic prompt for hours
+ # select_time(my_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
+ # select_time(my_time, prompt: { hour: true }) # generic prompt for hours
# select_time(my_time, prompt: true) # generic prompts for all
def select_time(datetime = Time.current, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_time
@@ -681,9 +681,8 @@ module ActionView
options = args.extract_options!
format = options.delete(:format) || :long
content = args.first || I18n.l(date_or_time, format: format)
- datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.iso8601
- content_tag("time".freeze, content, options.reverse_merge(datetime: datetime), &block)
+ content_tag("time".freeze, content, options.reverse_merge(datetime: date_or_time.iso8601), &block)
end
private
diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb
index 52dff1f750..88ceba414b 100644
--- a/actionview/lib/action_view/helpers/debug_helper.rb
+++ b/actionview/lib/action_view/helpers/debug_helper.rb
@@ -24,7 +24,7 @@ module ActionView
# created_at:
# </pre>
def debug(object)
- Marshal::dump(object)
+ Marshal.dump(object)
object = ERB::Util.html_escape(object.to_yaml)
content_tag(:pre, object, class: "debug_dump")
rescue # errors from Marshal or YAML
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index 6702c65ccb..2d5c5684c1 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -1,12 +1,12 @@
# frozen_string_literal: true
require "cgi"
-require_relative "date_helper"
-require_relative "tag_helper"
-require_relative "form_tag_helper"
-require_relative "active_model_helper"
-require_relative "../model_naming"
-require_relative "../record_identifier"
+require "action_view/helpers/date_helper"
+require "action_view/helpers/tag_helper"
+require "action_view/helpers/form_tag_helper"
+require "action_view/helpers/active_model_helper"
+require "action_view/model_naming"
+require "action_view/record_identifier"
require "active_support/core_ext/module/attribute_accessors"
require "active_support/core_ext/hash/slice"
require "active_support/core_ext/string/output_safety"
@@ -19,7 +19,7 @@ module ActionView
# compared to using vanilla HTML.
#
# Typically, a form designed to create or update a resource reflects the
- # identity of the resource in several ways: (i) the url that the form is
+ # identity of the resource in several ways: (i) the URL that the form is
# sent to (the form element's +action+ attribute) should result in a request
# being routed to the appropriate controller action (with the appropriate <tt>:id</tt>
# parameter in the case of an existing resource), (ii) input fields should
@@ -166,7 +166,7 @@ module ActionView
# So for example you may use a named route directly. When the model is
# represented by a string or symbol, as in the example above, if the
# <tt>:url</tt> option is not specified, by default the form will be
- # sent back to the current url (We will describe below an alternative
+ # sent back to the current URL (We will describe below an alternative
# resource-oriented usage of +form_for+ in which the URL does not need
# to be specified explicitly).
# * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of
@@ -478,6 +478,8 @@ module ActionView
mattr_accessor :form_with_generates_remote_forms, default: true
+ mattr_accessor :form_with_generates_ids, default: false
+
# Creates a form tag based on mixing URLs, scopes, or models.
#
# # Using just a URL:
@@ -606,10 +608,10 @@ module ActionView
# This is helpful when fragment-caching the form. Remote forms
# get the authenticity token from the <tt>meta</tt> tag, so embedding is
# unnecessary unless you support browsers without JavaScript.
- # * <tt>:local</tt> - By default form submits are remote and unobstrusive XHRs.
+ # * <tt>:local</tt> - By default form submits are remote and unobtrusive XHRs.
# Disable remote submits with <tt>local: true</tt>.
- # * <tt>:skip_enforcing_utf8</tt> - By default a hidden field named +utf8+
- # is output to enforce UTF-8 submits. Set to true to skip the field.
+ # * <tt>:skip_enforcing_utf8</tt> - If set to true, a hidden input with name
+ # utf8 is not output.
# * <tt>:builder</tt> - Override the object used to build the form.
# * <tt>:id</tt> - Optional HTML id attribute.
# * <tt>:class</tt> - Optional HTML class attribute.
@@ -640,16 +642,6 @@ module ActionView
#
# Where <tt>@document = Document.find(params[:id])</tt>.
#
- # When using labels +form_with+ requires setting the id on the field being
- # labelled:
- #
- # <%= form_with(model: @post) do |form| %>
- # <%= form.label :title %>
- # <%= form.text_field :title, id: :post_title %>
- # <% end %>
- #
- # See +label+ for more on how the +for+ attribute is derived.
- #
# === Mixing with other form helpers
#
# While +form_with+ uses a FormBuilder object it's possible to mix and
@@ -746,7 +738,7 @@ module ActionView
# end
def form_with(model: nil, scope: nil, url: nil, format: nil, **options)
options[:allow_method_names_outside_object] = true
- options[:skip_default_ids] = true
+ options[:skip_default_ids] = !form_with_generates_ids
if model
url ||= polymorphic_path(model, format: format)
@@ -1022,14 +1014,13 @@ module ActionView
# <%= fields :comment do |fields| %>
# <%= fields.text_field :body %>
# <% end %>
- # # => <input type="text" name="comment[body]>
+ # # => <input type="text" name="comment[body]">
#
# # Using a model infers the scope and assigns field values:
- # <%= fields model: Comment.new(body: "full bodied") do |fields| %<
+ # <%= fields model: Comment.new(body: "full bodied") do |fields| %>
# <%= fields.text_field :body %>
# <% end %>
- # # =>
- # <input type="text" name="comment[body] value="full bodied">
+ # # => <input type="text" name="comment[body]" value="full bodied">
#
# # Using +fields+ with +form_with+:
# <%= form_with model: @post do |form| %>
@@ -1044,16 +1035,6 @@ module ActionView
# or model is yielded, so any generated field names are prefixed with
# either the passed scope or the scope inferred from the <tt>:model</tt>.
#
- # When using labels +fields+ requires setting the id on the field being
- # labelled:
- #
- # <%= fields :comment do |fields| %>
- # <%= fields.label :body %>
- # <%= fields.text_field :body, id: :comment_body %>
- # <% end %>
- #
- # See +label+ for more on how the +for+ attribute is derived.
- #
# === Mixing with other form helpers
#
# While +form_with+ uses a FormBuilder object it's possible to mix and
@@ -1072,7 +1053,7 @@ module ActionView
# FormOptionsHelper#collection_select and DateHelper#datetime_select.
def fields(scope = nil, model: nil, **options, &block)
options[:allow_method_names_outside_object] = true
- options[:skip_default_ids] = true
+ options[:skip_default_ids] = !form_with_generates_ids
if model
scope ||= model_name_from_record_or_class(model).param_key
@@ -1538,10 +1519,10 @@ module ActionView
private
def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms,
- skip_enforcing_utf8: false, **options)
+ skip_enforcing_utf8: nil, **options)
html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html)
html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted?
- html_options[:enforce_utf8] = !skip_enforcing_utf8
+ html_options[:enforce_utf8] = !skip_enforcing_utf8 unless skip_enforcing_utf8.nil?
html_options[:enctype] = "multipart/form-data" if html_options.delete(:multipart)
@@ -1985,11 +1966,11 @@ module ActionView
# See the docs for the <tt>ActionView::FormHelper.fields</tt> helper method.
def fields(scope = nil, model: nil, **options, &block)
options[:allow_method_names_outside_object] = true
- options[:skip_default_ids] = true
+ options[:skip_default_ids] = !FormHelper.form_with_generates_ids
convert_to_legacy_options(options)
- fields_for(scope || model, model, **options, &block)
+ fields_for(scope || model, model, options, &block)
end
# Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
@@ -2265,7 +2246,7 @@ module ActionView
@template.button_tag(value, options, &block)
end
- def emitted_hidden_id?
+ def emitted_hidden_id? # :nodoc:
@emitted_hidden_id ||= nil
end
@@ -2285,7 +2266,12 @@ module ActionView
end
defaults = []
- defaults << :"helpers.submit.#{object_name}.#{key}"
+ # Object is a model and it is not overwritten by as and scope option.
+ if object.respond_to?(:model_name) && object_name.to_s == model.downcase
+ defaults << :"helpers.submit.#{object.model_name.i18n_key}.#{key}"
+ else
+ defaults << :"helpers.submit.#{object_name}.#{key}"
+ end
defaults << :"helpers.submit.#{key}"
defaults << "#{key.to_s.humanize} #{model}"
diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb
index 1517abfad0..d02f641867 100644
--- a/actionview/lib/action_view/helpers/form_options_helper.rb
+++ b/actionview/lib/action_view/helpers/form_options_helper.rb
@@ -2,7 +2,7 @@
require "cgi"
require "erb"
-require_relative "form_helper"
+require "action_view/helpers/form_helper"
require "active_support/core_ext/string/output_safety"
require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/array/wrap"
@@ -16,7 +16,7 @@ module ActionView
#
# * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element.
#
- # select("post", "category", Post::CATEGORIES, {include_blank: true})
+ # select("post", "category", Post::CATEGORIES, { include_blank: true })
#
# could become:
#
@@ -30,7 +30,7 @@ module ActionView
#
# Example with <tt>@post.person_id => 2</tt>:
#
- # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'})
+ # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: 'None' })
#
# could become:
#
@@ -43,7 +43,7 @@ module ActionView
#
# * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string.
#
- # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'})
+ # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { prompt: 'Select Person' })
#
# could become:
#
@@ -69,7 +69,7 @@ module ActionView
#
# * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output.
#
- # select("post", "category", Post::CATEGORIES, {disabled: 'restricted'})
+ # select("post", "category", Post::CATEGORIES, { disabled: 'restricted' })
#
# could become:
#
@@ -82,7 +82,7 @@ module ActionView
#
# When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled.
#
- # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: -> (category) { category.archived? }})
+ # collection_select(:post, :category_id, Category.all, :id, :name, { disabled: -> (category) { category.archived? } })
#
# If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return:
# <select name="post[category_id]" id="post_category_id">
@@ -107,7 +107,7 @@ module ActionView
#
# For example:
#
- # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { include_blank: true })
+ # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true })
#
# would become:
#
@@ -214,9 +214,13 @@ module ActionView
# * +method+ - The attribute of +object+ corresponding to the select tag
# * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
# * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
- # array of child objects representing the <tt><option></tt> tags.
+ # array of child objects representing the <tt><option></tt> tags. It can also be any object that responds
+ # to +call+, such as a +proc+, that will be called for each member of the +collection+ to retrieve the
+ # value.
# * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
- # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
+ # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag. It can also be any object
+ # that responds to +call+, such as a +proc+, that will be called for each member of the +collection+ to
+ # retrieve the label.
# * +option_key_method+ - The name of a method which, when called on a child object of a member of
# +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
# * +option_value_method+ - The name of a method which, when called on a child object of a member of
@@ -279,17 +283,17 @@ module ActionView
# Finally, this method supports a <tt>:default</tt> option, which selects
# a default ActiveSupport::TimeZone if the object's time zone is +nil+.
#
- # time_zone_select( "user", "time_zone", nil, include_blank: true)
+ # time_zone_select("user", "time_zone", nil, include_blank: true)
#
- # time_zone_select( "user", "time_zone", nil, default: "Pacific Time (US & Canada)" )
+ # time_zone_select("user", "time_zone", nil, default: "Pacific Time (US & Canada)")
#
- # time_zone_select( "user", 'time_zone', ActiveSupport::TimeZone.us_zones, default: "Pacific Time (US & Canada)")
+ # time_zone_select("user", 'time_zone', ActiveSupport::TimeZone.us_zones, default: "Pacific Time (US & Canada)")
#
- # time_zone_select( "user", 'time_zone', [ ActiveSupport::TimeZone['Alaska'], ActiveSupport::TimeZone['Hawaii'] ])
+ # time_zone_select("user", 'time_zone', [ ActiveSupport::TimeZone['Alaska'], ActiveSupport::TimeZone['Hawaii'] ])
#
- # time_zone_select( "user", 'time_zone', /Australia/)
+ # time_zone_select("user", 'time_zone', /Australia/)
#
- # time_zone_select( "user", "time_zone", ActiveSupport::TimeZone.all.sort, model: ActiveSupport::TimeZone)
+ # time_zone_select("user", "time_zone", ActiveSupport::TimeZone.all.sort, model: ActiveSupport::TimeZone)
def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
Tags::TimeZoneSelect.new(object, method, self, priority_zones, options, html_options).render
end
@@ -319,12 +323,12 @@ module ActionView
#
# You can optionally provide HTML attributes as the last element of the array.
#
- # options_for_select([ "Denmark", ["USA", {class: 'bold'}], "Sweden" ], ["USA", "Sweden"])
+ # options_for_select([ "Denmark", ["USA", { class: 'bold' }], "Sweden" ], ["USA", "Sweden"])
# # => <option value="Denmark">Denmark</option>
# # => <option value="USA" class="bold" selected="selected">USA</option>
# # => <option value="Sweden" selected="selected">Sweden</option>
#
- # options_for_select([["Dollar", "$", {class: "bold"}], ["Kroner", "DKK", {onclick: "alert('HI');"}]])
+ # options_for_select([["Dollar", "$", { class: "bold" }], ["Kroner", "DKK", { onclick: "alert('HI');" }]])
# # => <option value="$" class="bold">Dollar</option>
# # => <option value="DKK" onclick="alert('HI');">Kroner</option>
#
@@ -457,9 +461,9 @@ module ActionView
def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
collection.map do |group|
option_tags = options_from_collection_for_select(
- group.send(group_method), option_key_method, option_value_method, selected_key)
+ value_for_collection(group, group_method), option_key_method, option_value_method, selected_key)
- content_tag("optgroup".freeze, option_tags, label: group.send(group_label_method))
+ content_tag("optgroup".freeze, option_tags, label: value_for_collection(group, group_label_method))
end.join.html_safe
end
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index 31a1f8be8c..ba09738beb 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "cgi"
-require_relative "tag_helper"
+require "action_view/helpers/tag_helper"
require "active_support/core_ext/string/output_safety"
require "active_support/core_ext/module/attribute_accessors"
@@ -22,6 +22,8 @@ module ActionView
mattr_accessor :embed_authenticity_token_in_remote_forms
self.embed_authenticity_token_in_remote_forms = nil
+ mattr_accessor :default_enforce_utf8, default: true
+
# Starts a form tag that points the action to a url configured with <tt>url_for_options</tt> just like
# ActionController::Base#url_for. The method for the form defaults to POST.
#
@@ -115,7 +117,7 @@ module ActionView
# # <option>Write</option></select>
#
# select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: true
- # # => <select id="people" name="people"><option value=""></option><option value="1">David</option></select>
+ # # => <select id="people" name="people"><option value="" label=" "></option><option value="1">David</option></select>
#
# select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: "All"
# # => <select id="people" name="people"><option value="">All</option><option value="1">David</option></select>
@@ -387,14 +389,14 @@ module ActionView
# * Any other key creates standard HTML options for the tag.
#
# ==== Examples
- # radio_button_tag 'gender', 'male'
- # # => <input id="gender_male" name="gender" type="radio" value="male" />
+ # radio_button_tag 'favorite_color', 'maroon'
+ # # => <input id="favorite_color_maroon" name="favorite_color" type="radio" value="maroon" />
#
# radio_button_tag 'receive_updates', 'no', true
# # => <input checked="checked" id="receive_updates_no" name="receive_updates" type="radio" value="no" />
#
# radio_button_tag 'time_slot', "3:00 p.m.", false, disabled: true
- # # => <input disabled="disabled" id="time_slot_300_pm" name="time_slot" type="radio" value="3:00 p.m." />
+ # # => <input disabled="disabled" id="time_slot_3:00_p.m." name="time_slot" type="radio" value="3:00 p.m." />
#
# radio_button_tag 'color', "green", true, class: "color_input"
# # => <input checked="checked" class="color_input" id="color_green" name="color" type="radio" value="green" />
@@ -549,7 +551,8 @@ module ActionView
# # => <input src="/assets/save.png" data-confirm="Are you sure?" type="image" />
def image_submit_tag(source, options = {})
options = options.stringify_keys
- tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options)
+ src = path_to_image(source, skip_pipeline: options.delete("skip_pipeline"))
+ tag :input, { "type" => "image", "src" => src }.update(options)
end
# Creates a field set for grouping HTML form elements.
@@ -866,7 +869,7 @@ module ActionView
})
end
- if html_options.delete("enforce_utf8") { true }
+ if html_options.delete("enforce_utf8") { default_enforce_utf8 }
utf8_enforcer_tag + method_tag
else
method_tag
diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb
index 11eefe0ee0..830088bea3 100644
--- a/actionview/lib/action_view/helpers/javascript_helper.rb
+++ b/actionview/lib/action_view/helpers/javascript_helper.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "tag_helper"
+require "action_view/helpers/tag_helper"
module ActionView
module Helpers #:nodoc:
@@ -63,6 +63,13 @@ module ActionView
# <%= javascript_tag defer: 'defer' do -%>
# alert('All is good')
# <% end -%>
+ #
+ # If you have a content security policy enabled then you can add an automatic
+ # nonce value by passing <tt>nonce: true</tt> as part of +html_options+. Example:
+ #
+ # <%= javascript_tag nonce: true do -%>
+ # alert('All is good')
+ # <% end -%>
def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block)
content =
if block_given?
@@ -72,6 +79,10 @@ module ActionView
content_or_options_with_block
end
+ if html_options[:nonce] == true
+ html_options[:nonce] = content_security_policy_nonce
+ end
+
content_tag("script".freeze, javascript_cdata_section(content), html_options)
end
diff --git a/actionview/lib/action_view/helpers/record_tag_helper.rb b/actionview/lib/action_view/helpers/record_tag_helper.rb
deleted file mode 100644
index a6953ee905..0000000000
--- a/actionview/lib/action_view/helpers/record_tag_helper.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module ActionView
- module Helpers #:nodoc:
- module RecordTagHelper
- def div_for(*) # :nodoc:
- raise NoMethodError, "The `div_for` method has been removed from " \
- "Rails. To continue using it, add the `record_tag_helper` gem to " \
- "your Gemfile:\n" \
- " gem 'record_tag_helper', '~> 1.0'\n" \
- "Consult the Rails upgrade guide for details."
- end
-
- def content_tag_for(*) # :nodoc:
- raise NoMethodError, "The `content_tag_for` method has been removed from " \
- "Rails. To continue using it, add the `record_tag_helper` gem to " \
- "your Gemfile:\n" \
- " gem 'record_tag_helper', '~> 1.0'\n" \
- "Consult the Rails upgrade guide for details."
- end
- end
- end
-end
diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb
index a64d7e396e..d12989ea64 100644
--- a/actionview/lib/action_view/helpers/tag_helper.rb
+++ b/actionview/lib/action_view/helpers/tag_helper.rb
@@ -88,9 +88,10 @@ module ActionView
if value.is_a?(Array)
value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze)
else
- value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
+ value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s.dup
end
- %(#{key}="#{value.gsub('"'.freeze, '&quot;'.freeze)}")
+ value.gsub!('"'.freeze, "&quot;".freeze)
+ %(#{key}="#{value}")
end
private
@@ -166,7 +167,7 @@ module ActionView
# This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt>
# from 1.4.3.
#
- # tag.div data: { city_state: %w( Chigaco IL ) }
+ # tag.div data: { city_state: %w( Chicago IL ) }
# # => <div data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]"></div>
#
# The generated attributes are escaped by default. This can be disabled using
@@ -227,10 +228,10 @@ module ActionView
# tag("img", src: "open & shut.png")
# # => <img src="open &amp; shut.png" />
#
- # tag("img", {src: "open &amp; shut.png"}, false, false)
+ # tag("img", { src: "open &amp; shut.png" }, false, false)
# # => <img src="open &amp; shut.png" />
#
- # tag("div", data: {name: 'Stephen', city_state: %w(Chicago IL)})
+ # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) })
# # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
def tag(name = nil, options = nil, open = false, escape = true)
if name.nil?
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
index 8934a9894c..eef527d36f 100644
--- a/actionview/lib/action_view/helpers/tags/base.rb
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -97,7 +97,7 @@ module ActionView
index = name_and_id_index(options)
options["name"] = options.fetch("name") { tag_name(options["multiple"], index) }
- unless skip_default_ids?
+ if generate_ids?
options["id"] = options.fetch("id") { tag_id(index) }
if namespace = options.delete("namespace")
options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace
@@ -109,11 +109,11 @@ module ActionView
# a little duplication to construct less strings
case
when @object_name.empty?
- "#{sanitized_method_name}#{"[]" if multiple}"
+ "#{sanitized_method_name}#{multiple ? "[]" : ""}"
when index
- "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}"
+ "#{@object_name}[#{index}][#{sanitized_method_name}]#{multiple ? "[]" : ""}"
else
- "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}"
+ "#{@object_name}[#{sanitized_method_name}]#{multiple ? "[]" : ""}"
end
end
@@ -170,7 +170,11 @@ module ActionView
option_tags = tag_builder.content_tag_string("option", options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, value: "") + "\n" + option_tags
end
if value.blank? && options[:prompt]
- option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), value: "") + "\n" + option_tags
+ tag_options = { value: "" }.tap do |prompt_opts|
+ prompt_opts[:disabled] = true if options[:disabled] == ""
+ prompt_opts[:selected] = true if options[:selected] == ""
+ end
+ option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
end
option_tags
end
@@ -183,8 +187,8 @@ module ActionView
end
end
- def skip_default_ids?
- @skip_default_ids
+ def generate_ids?
+ !@skip_default_ids
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/check_box.rb b/actionview/lib/action_view/helpers/tags/check_box.rb
index 6b34dfef90..4327e07cae 100644
--- a/actionview/lib/action_view/helpers/tags/check_box.rb
+++ b/actionview/lib/action_view/helpers/tags/check_box.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "checkable"
+require "action_view/helpers/tags/checkable"
module ActionView
module Helpers
diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
index 91c1135d20..455442178e 100644
--- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "collection_helpers"
+require "action_view/helpers/tags/collection_helpers"
module ActionView
module Helpers
diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
index 0b0482f74e..16d37134e5 100644
--- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "collection_helpers"
+require "action_view/helpers/tags/collection_helpers"
module ActionView
module Helpers
diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb
index 56b48bbd62..02bd099784 100644
--- a/actionview/lib/action_view/helpers/tags/label.rb
+++ b/actionview/lib/action_view/helpers/tags/label.rb
@@ -75,10 +75,6 @@ module ActionView
def render_component(builder)
builder.translation
end
-
- def skip_default_ids?
- false # The id is used as the `for` attribute.
- end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/radio_button.rb b/actionview/lib/action_view/helpers/tags/radio_button.rb
index 3cfdcbea3f..621db2b1b5 100644
--- a/actionview/lib/action_view/helpers/tags/radio_button.rb
+++ b/actionview/lib/action_view/helpers/tags/radio_button.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "checkable"
+require "action_view/helpers/tags/checkable"
module ActionView
module Helpers
diff --git a/actionview/lib/action_view/helpers/tags/text_area.rb b/actionview/lib/action_view/helpers/tags/text_area.rb
index 9c162b59f5..4519082ff6 100644
--- a/actionview/lib/action_view/helpers/tags/text_area.rb
+++ b/actionview/lib/action_view/helpers/tags/text_area.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "placeholderable"
+require "action_view/helpers/tags/placeholderable"
module ActionView
module Helpers
diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb
index 3553942048..d92967e212 100644
--- a/actionview/lib/action_view/helpers/tags/text_field.rb
+++ b/actionview/lib/action_view/helpers/tags/text_field.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "placeholderable"
+require "action_view/helpers/tags/placeholderable"
module ActionView
module Helpers
diff --git a/actionview/lib/action_view/helpers/tags/translator.rb b/actionview/lib/action_view/helpers/tags/translator.rb
index fcf96d2c9c..e81ca3aef0 100644
--- a/actionview/lib/action_view/helpers/tags/translator.rb
+++ b/actionview/lib/action_view/helpers/tags/translator.rb
@@ -16,13 +16,8 @@ module ActionView
translated_attribute || human_attribute_name
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :object_name, :method_and_value, :scope, :model
-
private
+ attr_reader :object_name, :method_and_value, :scope, :model
def i18n_default
if model
diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb
index 3044a2c0ef..34138de00e 100644
--- a/actionview/lib/action_view/helpers/text_helper.rb
+++ b/actionview/lib/action_view/helpers/text_helper.rb
@@ -13,9 +13,9 @@ module ActionView
#
# ==== Sanitization
#
- # Most text helpers by default sanitize the given content, but do not escape it.
- # This means HTML tags will appear in the page but all malicious code will be removed.
- # Let's look at some examples using the +simple_format+ method:
+ # Most text helpers that generate HTML output sanitize the given input by default,
+ # but do not escape it. This means HTML tags will appear in the page but all malicious
+ # code will be removed. Let's look at some examples using the +simple_format+ method:
#
# simple_format('<a href="http://example.com/">Example</a>')
# # => "<p><a href=\"http://example.com/\">Example</a></p>"
@@ -128,7 +128,7 @@ module ActionView
# # => You searched for: <a href="search?q=rails">rails</a>
#
# highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false)
- # # => "<a>ruby</a> on <mark>rails</mark>"
+ # # => <a href="javascript:alert('no!')">ruby</a> on <mark>rails</mark>
def highlight(text, phrases, options = {})
text = sanitize(text) if options.fetch(:sanitize, true)
@@ -422,7 +422,7 @@ module ActionView
def to_s
value = @values[@index].to_s
@index = next_index
- return value
+ value
end
private
@@ -446,7 +446,7 @@ module ActionView
# uses an instance variable of ActionView::Base.
def get_cycle(name)
@_cycles = Hash.new unless defined?(@_cycles)
- return @_cycles[name]
+ @_cycles[name]
end
def set_cycle(name, cycle_object)
diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
index e663892592..db44fdbfee 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "tag_helper"
+require "action_view/helpers/tag_helper"
require "active_support/core_ext/string/access"
require "i18n/exceptions"
@@ -60,7 +60,11 @@ module ActionView
def translate(key, options = {})
options = options.dup
has_default = options.has_key?(:default)
- remaining_defaults = Array(options.delete(:default)).compact
+ if has_default
+ remaining_defaults = Array(options.delete(:default)).compact
+ else
+ remaining_defaults = []
+ end
if has_default && !remaining_defaults.first.kind_of?(Symbol)
options[:default] = remaining_defaults
@@ -122,9 +126,12 @@ module ActionView
private
def scope_key_by_partial(key)
- if key.to_s.first == "."
+ stringified_key = key.to_s
+ if stringified_key.first == "."
if @virtual_path
- @virtual_path.gsub(%r{/_?}, ".") + key.to_s
+ @_scope_key_by_partial_cache ||= {}
+ @_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".")
+ "#{@_scope_key_by_partial_cache[@virtual_path]}#{stringified_key}"
else
raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
end
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index 2d5aac6dc7..cae62f2312 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "javascript_helper"
+require "action_view/helpers/javascript_helper"
require "active_support/core_ext/array/access"
require "active_support/core_ext/hash/keys"
require "active_support/core_ext/string/output_safety"
@@ -139,6 +139,11 @@ module ActionView
# link_to "Profiles", controller: "profiles"
# # => <a href="/profiles">Profiles</a>
#
+ # When name is +nil+ the href is presented instead
+ #
+ # link_to nil, "http://example.com"
+ # # => <a href="http://www.example.com">http://www.example.com</a>
+ #
# You can use a block as well if your link target is hard to fit into the name parameter. ERB example:
#
# <%= link_to(@profile) do %>
@@ -589,10 +594,27 @@ module ActionView
end
def add_method_to_attributes!(html_options, method)
- if method && method.to_s.downcase != "get".freeze && html_options["rel".freeze] !~ /nofollow/
- html_options["rel".freeze] = "#{html_options["rel".freeze]} nofollow".lstrip
+ if method_not_get_method?(method) && html_options["rel"] !~ /nofollow/
+ if html_options["rel"].blank?
+ html_options["rel"] = "nofollow"
+ else
+ html_options["rel"] = "#{html_options["rel"]} nofollow"
+ end
end
- html_options["data-method".freeze] = method
+ html_options["data-method"] = method
+ end
+
+ STRINGIFIED_COMMON_METHODS = {
+ get: "get",
+ delete: "delete",
+ patch: "patch",
+ post: "post",
+ put: "put",
+ }.freeze
+
+ def method_not_get_method?(method)
+ return false unless method
+ (STRINGIFIED_COMMON_METHODS[method] || method.to_s.downcase) != "get"
end
def token_tag(token = nil, form_options: {})
@@ -614,7 +636,7 @@ module ActionView
# to_form_params(name: 'David', nationality: 'Danish')
# # => [{name: :name, value: 'David'}, {name: 'nationality', value: 'Danish'}]
#
- # to_form_params(country: {name: 'Denmark'})
+ # to_form_params(country: { name: 'Denmark' })
# # => [{name: 'country[name]', value: 'Denmark'}]
#
# to_form_params(countries: ['Denmark', 'Sweden']})
diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb
index b11ef6e133..3e6d352c15 100644
--- a/actionview/lib/action_view/layouts.rb
+++ b/actionview/lib/action_view/layouts.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "rendering"
+require "action_view/rendering"
require "active_support/core_ext/module/redefine_method"
module ActionView
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index acc0f57f1d..0e56eca35c 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -3,7 +3,7 @@
require "concurrent/map"
require "active_support/core_ext/module/remove_method"
require "active_support/core_ext/module/attribute_accessors"
-require_relative "template/resolver"
+require "action_view/template/resolver"
module ActionView
# = Action View Lookup Context
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index b22347c55c..12d06bf376 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -9,6 +9,8 @@ module ActionView
config.action_view = ActiveSupport::OrderedOptions.new
config.action_view.embed_authenticity_token_in_remote_forms = nil
config.action_view.debug_missing_translation = true
+ config.action_view.default_enforce_utf8 = nil
+ config.action_view.finalize_compiled_template_methods = true
config.eager_load_namespaces << ActionView
@@ -22,12 +24,35 @@ module ActionView
initializer "action_view.form_with_generates_remote_forms" do |app|
ActiveSupport.on_load(:action_view) do
form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms)
- unless form_with_generates_remote_forms.nil?
- ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms
+ ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms
+ end
+ end
+
+ initializer "action_view.form_with_generates_ids" do |app|
+ ActiveSupport.on_load(:action_view) do
+ form_with_generates_ids = app.config.action_view.delete(:form_with_generates_ids)
+ unless form_with_generates_ids.nil?
+ ActionView::Helpers::FormHelper.form_with_generates_ids = form_with_generates_ids
end
end
end
+ initializer "action_view.default_enforce_utf8" do |app|
+ ActiveSupport.on_load(:action_view) do
+ default_enforce_utf8 = app.config.action_view.delete(:default_enforce_utf8)
+ unless default_enforce_utf8.nil?
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = default_enforce_utf8
+ end
+ end
+ end
+
+ initializer "action_view.finalize_compiled_template_methods" do |app|
+ ActiveSupport.on_load(:action_view) do
+ ActionView::Template.finalize_compiled_template_methods =
+ app.config.action_view.delete(:finalize_compiled_template_methods)
+ end
+ end
+
initializer "action_view.logger" do
ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
end
diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb
index b34a793c89..1310a1ce0a 100644
--- a/actionview/lib/action_view/record_identifier.rb
+++ b/actionview/lib/action_view/record_identifier.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "active_support/core_ext/module"
-require_relative "model_naming"
+require "action_view/model_naming"
module ActionView
# RecordIdentifier encapsulates methods used by various ActionView helpers
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index 8534eeb518..d7f97c3b50 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "concurrent/map"
-require_relative "partial_renderer/collection_caching"
+require "action_view/renderer/partial_renderer/collection_caching"
module ActionView
class PartialIteration
diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
index ca49eb1144..276a28ce07 100644
--- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb
+++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
@@ -65,7 +65,9 @@ module ActionView
yielder = lambda { |*name| view._layout_for(*name) }
instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do
+ outer_config = I18n.config
fiber = Fiber.new do
+ I18n.config = outer_config
if layout
layout.render(view, locals, output, &yielder)
else
diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb
index 2648f9153f..4e5fdfbb2d 100644
--- a/actionview/lib/action_view/rendering.rb
+++ b/actionview/lib/action_view/rendering.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "view_paths"
+require "action_view/view_paths"
module ActionView
# This is a class to fix I18n global state. Whenever you provide I18n.locale during a request,
@@ -75,8 +75,7 @@ module ActionView
end
# Returns an object that is able to render templates.
- # :api: private
- def view_renderer
+ def view_renderer # :nodoc:
@_view_renderer ||= ActionView::Renderer.new(lookup_context)
end
@@ -92,7 +91,6 @@ module ActionView
private
# Find and render a template based on the options given.
- # :api: private
def _render_template(options)
variant = options.delete(:variant)
assigns = options.delete(:assigns)
@@ -114,7 +112,6 @@ module ActionView
# Normalize args by converting render "foo" to render :action => "foo" and
# render "foo/bar" to render :template => "foo/bar".
- # :api: private
def _normalize_args(action = nil, options = {})
options = super(action, options)
case action
@@ -137,7 +134,6 @@ module ActionView
end
# Normalize options.
- # :api: private
def _normalize_options(options)
options = super(options)
if options[:partial] == true
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 0c4bb73acb..ee1cd61f12 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -9,6 +9,8 @@ module ActionView
class Template
extend ActiveSupport::Autoload
+ mattr_accessor :finalize_compiled_template_methods, default: true
+
# === Encodings in ActionView::Template
#
# ActionView::Template is one of a few sources of potential
@@ -307,7 +309,9 @@ module ActionView
end
mod.module_eval(source, identifier, 0)
- ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
+ if finalize_compiled_template_methods
+ ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
+ end
end
def handle_render_error(view, e)
diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb
index 2b0b25817b..4e3c02e05e 100644
--- a/actionview/lib/action_view/template/error.rb
+++ b/actionview/lib/action_view/template/error.rb
@@ -10,9 +10,6 @@ module ActionView
class EncodingError < StandardError #:nodoc:
end
- class MissingRequestError < StandardError #:nodoc:
- end
-
class WrongEncodingError < EncodingError #:nodoc:
def initialize(string, encoding)
@string, @encoding = string, encoding
diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb
index c41de62e52..b7b749f9da 100644
--- a/actionview/lib/action_view/template/handlers/erb.rb
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -3,11 +3,8 @@
module ActionView
class Template
module Handlers
- autoload :Erubis, "action_view/template/handlers/erb/deprecated_erubis"
-
class ERB
autoload :Erubi, "action_view/template/handlers/erb/erubi"
- autoload :Erubis, "action_view/template/handlers/erb/erubis"
# Specify trim mode for the ERB compiler. Defaults to '-'.
# See ERB documentation for suitable values.
diff --git a/actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb b/actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb
deleted file mode 100644
index 00c7d7cc7d..0000000000
--- a/actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-::ActiveSupport::Deprecation.warn("ActionView::Template::Handlers::Erubis is deprecated and will be removed from Rails 5.2. Switch to ActionView::Template::Handlers::ERB::Erubi instead.")
-
-module ActionView
- class Template
- module Handlers
- Erubis = ERB::Erubis
- end
- end
-end
diff --git a/actionview/lib/action_view/template/handlers/erb/erubis.rb b/actionview/lib/action_view/template/handlers/erb/erubis.rb
deleted file mode 100644
index dc3bf1ff27..0000000000
--- a/actionview/lib/action_view/template/handlers/erb/erubis.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-# frozen_string_literal: true
-
-gem "erubis"
-require "erubis"
-
-module ActionView
- class Template
- module Handlers
- class ERB
- class Erubis < ::Erubis::Eruby
- # :nodoc: all
- def add_preamble(src)
- @newline_pending = 0
- src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;"
- end
-
- def add_text(src, text)
- return if text.empty?
-
- if text == "\n"
- @newline_pending += 1
- else
- src << "@output_buffer.safe_append='"
- src << "\n" * @newline_pending if @newline_pending > 0
- src << escape_text(text)
- src << "'.freeze;"
-
- @newline_pending = 0
- end
- end
-
- # Erubis toggles <%= and <%== behavior when escaping is enabled.
- # We override to always treat <%== as escaped.
- def add_expr(src, code, indicator)
- case indicator
- when "=="
- add_expr_escaped(src, code)
- else
- super
- end
- end
-
- BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
-
- def add_expr_literal(src, code)
- flush_newline_if_pending(src)
- if BLOCK_EXPR.match?(code)
- src << "@output_buffer.append= " << code
- else
- src << "@output_buffer.append=(" << code << ");"
- end
- end
-
- def add_expr_escaped(src, code)
- flush_newline_if_pending(src)
- if BLOCK_EXPR.match?(code)
- src << "@output_buffer.safe_expr_append= " << code
- else
- src << "@output_buffer.safe_expr_append=(" << code << ");"
- end
- end
-
- def add_stmt(src, code)
- flush_newline_if_pending(src)
- super
- end
-
- def add_postamble(src)
- flush_newline_if_pending(src)
- src << "@output_buffer.to_s"
- end
-
- def flush_newline_if_pending(src)
- if @newline_pending > 0
- src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;"
- @newline_pending = 0
- end
- end
- end
- end
- end
- end
-end
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index a58d375293..5a86f10973 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -3,7 +3,7 @@
require "pathname"
require "active_support/core_ext/class"
require "active_support/core_ext/module/attribute_accessors"
-require_relative "../template"
+require "action_view/template"
require "thread"
require "concurrent/map"
diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb
index 93be2be2d1..e1cbae5845 100644
--- a/actionview/lib/action_view/test_case.rb
+++ b/actionview/lib/action_view/test_case.rb
@@ -270,7 +270,7 @@ module ActionView
begin
routes = @controller.respond_to?(:_routes) && @controller._routes
rescue
- # Dont call routes, if there is an error on _routes call
+ # Don't call routes, if there is an error on _routes call
end
if routes &&
@@ -286,7 +286,7 @@ module ActionView
begin
routes = @controller.respond_to?(:_routes) && @controller._routes
rescue
- # Dont call routes, if there is an error on _routes call
+ # Don't call routes, if there is an error on _routes call
end
routes &&
diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb
index 92fdb24a5e..68186c3bf8 100644
--- a/actionview/lib/action_view/testing/resolvers.rb
+++ b/actionview/lib/action_view/testing/resolvers.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../template/resolver"
+require "action_view/template/resolver"
module ActionView #:nodoc:
# Use FixtureResolver in your tests to simulate the presence of files on the
diff --git a/actionview/package.json b/actionview/package.json
index 4cbf0207e5..624eb5de93 100644
--- a/actionview/package.json
+++ b/actionview/package.json
@@ -1,6 +1,6 @@
{
"name": "rails-ujs",
- "version": "5.2.0-alpha",
+ "version": "6.0.0-alpha",
"description": "Ruby on Rails unobtrusive scripting adapter",
"main": "lib/assets/compiled/rails-ujs.js",
"files": [
diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb
index c98270bd12..f20a66c2d2 100644
--- a/actionview/test/abstract_unit.rb
+++ b/actionview/test/abstract_unit.rb
@@ -26,14 +26,6 @@ require "active_record"
require "pp" # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late
-module Rails
- class << self
- def env
- @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test")
- end
- end
-end
-
ActiveSupport::Dependencies.hook!
Thread.abort_on_exception = true
@@ -110,12 +102,6 @@ module ActionDispatch
end
end
-module ActiveSupport
- class TestCase
- include ActionDispatch::DrawOnce
- end
-end
-
class RoutedRackApp
attr_reader :routes
@@ -162,29 +148,6 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
self.app = build_app
- # Stub Rails dispatcher so it does not get controller references and
- # simply return the controller#action as Rack::Body.
- class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher
- private
- def controller_reference(controller_param)
- controller_param
- end
-
- def dispatch(controller, action, env)
- [200, { "Content-Type" => "text/html" }, ["#{controller}##{action}"]]
- end
- end
-
- def self.stub_controllers
- old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher
- ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher }
- ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, StubDispatcher }
- yield ActionDispatch::Routing::RouteSet.new
- ensure
- ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher }
- ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher }
- end
-
def with_routing(&block)
temporary_routes = ActionDispatch::Routing::RouteSet.new
old_app, self.class.app = self.class.app, self.class.build_app(temporary_routes)
@@ -196,21 +159,6 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
self.class.app = old_app
silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) }
end
-
- def with_autoload_path(path)
- path = File.join(File.expand_path("fixtures", __dir__), path)
- if ActiveSupport::Dependencies.autoload_paths.include?(path)
- yield
- else
- begin
- ActiveSupport::Dependencies.autoload_paths << path
- yield
- ensure
- ActiveSupport::Dependencies.autoload_paths.reject! { |p| p == path }
- ActiveSupport::Dependencies.clear
- end
- end
- end
end
ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
@@ -274,6 +222,7 @@ module ActionDispatch
end
class ActiveSupport::TestCase
+ include ActionDispatch::DrawOnce
include ActiveSupport::Testing::MethodCallAssertions
# Skips the current run on Rubinius using Minitest::Assertions#skip
diff --git a/actionview/test/actionpack/abstract/abstract_controller_test.rb b/actionview/test/actionpack/abstract/abstract_controller_test.rb
index 468a6376c7..4d4e2b8ef2 100644
--- a/actionview/test/actionpack/abstract/abstract_controller_test.rb
+++ b/actionview/test/actionpack/abstract/abstract_controller_test.rb
@@ -236,7 +236,7 @@ module AbstractController
end
end
- class RespondToActionController < AbstractController::Base;
+ class RespondToActionController < AbstractController::Base
def index() self.response_body = "success" end
def fail() self.response_body = "fail" end
diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb
index 6e61b12285..3e6b55a87e 100644
--- a/actionview/test/actionpack/controller/render_test.rb
+++ b/actionview/test/actionpack/controller/render_test.rb
@@ -4,12 +4,8 @@ require "abstract_unit"
require "active_model"
require "controller/fake_models"
-class ApplicationController < ActionController::Base
- self.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack")
-end
-
module Quiz
- #Models
+ # Models
Question = Struct.new(:name, :id) do
extend ActiveModel::Naming
include ActiveModel::Conversion
@@ -20,7 +16,7 @@ module Quiz
end
# Controller
- class QuestionsController < ApplicationController
+ class QuestionsController < ActionController::Base
def new
render partial: Quiz::Question.new("Namespaced Partial")
end
@@ -28,7 +24,7 @@ module Quiz
end
module Fun
- class GamesController < ApplicationController
+ class GamesController < ActionController::Base
def hello_world; end
def nested_partial_with_form_builder
@@ -37,7 +33,7 @@ module Fun
end
end
-class TestController < ApplicationController
+class TestController < ActionController::Base
protect_from_forgery
before_action :set_variable_for_layout
@@ -644,10 +640,15 @@ class RenderTest < ActionController::TestCase
ActionView::Base.logger = ActiveSupport::Logger.new(nil)
@request.host = "www.nextangle.com"
+
+ @old_view_paths = ActionController::Base.view_paths
+ ActionController::Base.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack")
end
def teardown
ActionView::Base.logger = nil
+
+ ActionController::Base.view_paths = @old_view_paths
end
# :ported:
diff --git a/actionview/test/active_record_unit.rb b/actionview/test/active_record_unit.rb
index b39ecd8813..7f48b515a0 100644
--- a/actionview/test/active_record_unit.rb
+++ b/actionview/test/active_record_unit.rb
@@ -38,7 +38,7 @@ class ActiveRecordTestConnector
end
rescue Exception => e # errors from ActiveRecord setup
$stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}"
- #$stderr.puts " #{e.backtrace.join("\n ")}\n"
+ # $stderr.puts " #{e.backtrace.join("\n ")}\n"
self.able_to_connect = false
end
diff --git a/actionview/test/activerecord/multifetch_cache_test.rb b/actionview/test/activerecord/multifetch_cache_test.rb
new file mode 100644
index 0000000000..12be069e69
--- /dev/null
+++ b/actionview/test/activerecord/multifetch_cache_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "active_record_unit"
+require "active_record/railties/collection_cache_association_loading"
+
+ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading)
+
+class MultifetchCacheTest < ActiveRecordTestCase
+ fixtures :topics, :replies
+
+ def setup
+ view_paths = ActionController::Base.view_paths
+
+ @view = Class.new(ActionView::Base) do
+ def view_cache_dependencies
+ []
+ end
+
+ def combined_fragment_cache_key(key)
+ [ :views, key ]
+ end
+ end.new(view_paths, {})
+ end
+
+ def test_only_preloading_for_records_that_miss_the_cache
+ @view.render partial: "test/partial", collection: [topics(:rails)], cached: true
+
+ @topics = Topic.preload(:replies)
+
+ @view.render partial: "test/partial", collection: @topics, cached: true
+
+ assert_not @topics.detect { |topic| topic.id == topics(:rails).id }.replies.loaded?
+ assert @topics.detect { |topic| topic.id != topics(:rails).id }.replies.loaded?
+ end
+end
diff --git a/actionview/test/activerecord/render_partial_with_record_identification_test.rb b/actionview/test/activerecord/render_partial_with_record_identification_test.rb
index 367d2c3174..3a698fa42e 100644
--- a/actionview/test/activerecord/render_partial_with_record_identification_test.rb
+++ b/actionview/test/activerecord/render_partial_with_record_identification_test.rb
@@ -17,21 +17,11 @@ class RenderPartialWithRecordIdentificationController < ActionController::Base
render partial: Reply.base
end
- def render_with_has_many_through_association
- @developer = Developer.first
- render partial: @developer.topics
- end
-
def render_with_has_one_association
@company = Company.find(1)
render partial: @company.mascot
end
- def render_with_belongs_to_association
- @reply = Reply.find(1)
- render partial: @reply.topic
- end
-
def render_with_record
@developer = Developer.first
render partial: @developer
diff --git a/actionview/test/fixtures/digestor/comments/show.js.erb b/actionview/test/fixtures/digestor/comments/show.js.erb
new file mode 100644
index 0000000000..38b37dfa2b
--- /dev/null
+++ b/actionview/test/fixtures/digestor/comments/show.js.erb
@@ -0,0 +1 @@
+alert("<%=j render("comments/comment") %>")
diff --git a/actionview/test/fixtures/layouts/streaming_with_locale.erb b/actionview/test/fixtures/layouts/streaming_with_locale.erb
new file mode 100644
index 0000000000..e1fdad2073
--- /dev/null
+++ b/actionview/test/fixtures/layouts/streaming_with_locale.erb
@@ -0,0 +1,2 @@
+layout.locale: <%= I18n.locale %>
+<%= yield %>
diff --git a/actionview/test/fixtures/public/.gitignore b/actionview/test/fixtures/public/.gitignore
deleted file mode 100644
index 312e635ee6..0000000000
--- a/actionview/test/fixtures/public/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-absolute/*
diff --git a/actionview/test/fixtures/test/streaming_with_locale.erb b/actionview/test/fixtures/test/streaming_with_locale.erb
new file mode 100644
index 0000000000..b0f2b2f7e9
--- /dev/null
+++ b/actionview/test/fixtures/test/streaming_with_locale.erb
@@ -0,0 +1 @@
+view.locale: <%= I18n.locale %>
diff --git a/actionview/test/template/active_model_helper_test.rb b/actionview/test/template/active_model_helper_test.rb
index a929d8dc3d..36afed6dd6 100644
--- a/actionview/test/template/active_model_helper_test.rb
+++ b/actionview/test/template/active_model_helper_test.rb
@@ -6,7 +6,7 @@ class ActiveModelHelperTest < ActionView::TestCase
tests ActionView::Helpers::ActiveModelHelper
silence_warnings do
- Post = Struct.new(:author_name, :body, :updated_at) do
+ Post = Struct.new(:author_name, :body, :category, :published, :updated_at) do
include ActiveModel::Conversion
include ActiveModel::Validations
@@ -22,10 +22,14 @@ class ActiveModelHelperTest < ActionView::TestCase
@post = Post.new
@post.errors[:author_name] << "can't be empty"
@post.errors[:body] << "foo"
+ @post.errors[:category] << "must exist"
+ @post.errors[:published] << "must be accepted"
@post.errors[:updated_at] << "bar"
@post.author_name = ""
@post.body = "Back to the hill and over it again!"
+ @post.category = "rails"
+ @post.published = false
@post.updated_at = Date.new(2004, 6, 15)
end
@@ -56,6 +60,25 @@ class ActiveModelHelperTest < ActionView::TestCase
assert_dom_equal(expected_dom, select("post", "author_name", [:a, :b], prompt: "Choose one..."))
end
+ def test_select_grouped_options_with_errors
+ grouped_options = [
+ ["A", [["A1"], ["A2"]]],
+ ["B", [["B1"], ["B2"]]],
+ ]
+
+ assert_dom_equal(
+ %(<div class="field_with_errors"><select name="post[category]" id="post_category"><optgroup label="A"><option value="A1">A1</option>\n<option value="A2">A2</option></optgroup><optgroup label="B"><option value="B1">B1</option>\n<option value="B2">B2</option></optgroup></select></div>),
+ select("post", "category", grouped_options)
+ )
+ end
+
+ def test_collection_select_with_errors
+ assert_dom_equal(
+ %(<div class="field_with_errors"><select name="post[author_name]" id="post_author_name"><option value="a">a</option>\n<option value="b">b</option></select></div>),
+ collection_select("post", "author_name", [:a, :b], :to_s, :to_s)
+ )
+ end
+
def test_date_select_with_errors
assert_dom_equal(
%(<div class="field_with_errors"><select id="post_updated_at_1i" name="post[updated_at(1i)]">\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n</select>\n<input id="post_updated_at_2i" name="post[updated_at(2i)]" type="hidden" value="6" />\n<input id="post_updated_at_3i" name="post[updated_at(3i)]" type="hidden" value="1" />\n</div>),
@@ -77,6 +100,55 @@ class ActiveModelHelperTest < ActionView::TestCase
)
end
+ def test_label_with_errors
+ assert_dom_equal(
+ %(<div class="field_with_errors"><label for="post_body">Body</label></div>),
+ label("post", "body")
+ )
+ end
+
+ def test_check_box_with_errors
+ assert_dom_equal(
+ %(<input name="post[published]" type="hidden" value="0" /><div class="field_with_errors"><input type="checkbox" value="1" name="post[published]" id="post_published" /></div>),
+ check_box("post", "published")
+ )
+ end
+
+ def test_check_boxes_with_errors
+ assert_dom_equal(
+ %(<input name="post[published]" type="hidden" value="0" /><div class="field_with_errors"><input type="checkbox" value="1" name="post[published]" id="post_published" /></div><input name="post[published]" type="hidden" value="0" /><div class="field_with_errors"><input type="checkbox" value="1" name="post[published]" id="post_published" /></div>),
+ check_box("post", "published") + check_box("post", "published")
+ )
+ end
+
+ def test_radio_button_with_errors
+ assert_dom_equal(
+ %(<div class="field_with_errors"><input type="radio" value="rails" checked="checked" name="post[category]" id="post_category_rails" /></div>),
+ radio_button("post", "category", "rails")
+ )
+ end
+
+ def test_radio_buttons_with_errors
+ assert_dom_equal(
+ %(<div class="field_with_errors"><input type="radio" value="rails" checked="checked" name="post[category]" id="post_category_rails" /></div><div class="field_with_errors"><input type="radio" value="java" name="post[category]" id="post_category_java" /></div>),
+ radio_button("post", "category", "rails") + radio_button("post", "category", "java")
+ )
+ end
+
+ def test_collection_check_boxes_with_errors
+ assert_dom_equal(
+ %(<input type="hidden" name="post[category][]" value="" /><div class="field_with_errors"><input type="checkbox" value="ruby" name="post[category][]" id="post_category_ruby" /></div><label for="post_category_ruby">ruby</label><div class="field_with_errors"><input type="checkbox" value="java" name="post[category][]" id="post_category_java" /></div><label for="post_category_java">java</label>),
+ collection_check_boxes("post", "category", [:ruby, :java], :to_s, :to_s)
+ )
+ end
+
+ def test_collection_radio_buttons_with_errors
+ assert_dom_equal(
+ %(<input type="hidden" name="post[category]" value="" /><div class="field_with_errors"><input type="radio" value="ruby" name="post[category]" id="post_category_ruby" /></div><label for="post_category_ruby">ruby</label><div class="field_with_errors"><input type="radio" value="java" name="post[category]" id="post_category_java" /></div><label for="post_category_java">java</label>),
+ collection_radio_buttons("post", "category", [:ruby, :java], :to_s, :to_s)
+ )
+ end
+
def test_hidden_field_does_not_render_errors
assert_dom_equal(
%(<input id="post_author_name" name="post[author_name]" type="hidden" value="" />),
diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb
index 6645839f0e..e68f03d1f4 100644
--- a/actionview/test/template/asset_tag_helper_test.rb
+++ b/actionview/test/template/asset_tag_helper_test.rb
@@ -19,6 +19,7 @@ class AssetTagHelperTest < ActionView::TestCase
def ssl?() false end
def host_with_port() "localhost" end
def base_url() "http://www.example.com" end
+ def send_early_hints(links) end
end.new
@controller.request = @request
@@ -28,6 +29,10 @@ class AssetTagHelperTest < ActionView::TestCase
"http://www.example.com"
end
+ def content_security_policy_nonce
+ "iyhD0Yc0W+c="
+ end
+
AssetPathToTag = {
%(asset_path("")) => %(),
%(asset_path(" ")) => %(),
@@ -213,6 +218,17 @@ class AssetTagHelperTest < ActionView::TestCase
%(favicon_link_tag 'mb-icon.png', :rel => 'apple-touch-icon', :type => 'image/png') => %(<link href="/images/mb-icon.png" rel="apple-touch-icon" type="image/png" />)
}
+ PreloadLinkToTag = {
+ %(preload_link_tag '/styles/custom_theme.css') => %(<link rel="preload" href="/styles/custom_theme.css" as="style" type="text/css" />),
+ %(preload_link_tag '/videos/video.webm') => %(<link rel="preload" href="/videos/video.webm" as="video" type="video/webm" />),
+ %(preload_link_tag '/posts.json', as: 'fetch') => %(<link rel="preload" href="/posts.json" as="fetch" type="application/json" />),
+ %(preload_link_tag '/users', as: 'fetch', type: 'application/json') => %(<link rel="preload" href="/users" as="fetch" type="application/json" />),
+ %(preload_link_tag '//example.com/map?callback=initMap', as: 'fetch', type: 'application/javascript') => %(<link rel="preload" href="//example.com/map?callback=initMap" as="fetch" type="application/javascript" />),
+ %(preload_link_tag '//example.com/font.woff2') => %(<link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>),
+ %(preload_link_tag '//example.com/font.woff2', crossorigin: 'use-credentials') => %(<link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" />),
+ %(preload_link_tag '/media/audio.ogg', nopush: true) => %(<link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" />)
+ }
+
VideoPathToTag = {
%(video_path("xml")) => %(/videos/xml),
%(video_path("xml.ogg")) => %(/videos/xml.ogg),
@@ -305,6 +321,24 @@ class AssetTagHelperTest < ActionView::TestCase
%(font_path("font.ttf?123")) => %(/fonts/font.ttf?123)
}
+ FontUrlToTag = {
+ %(font_url("font.eot")) => %(http://www.example.com/fonts/font.eot),
+ %(font_url("font.eot#iefix")) => %(http://www.example.com/fonts/font.eot#iefix),
+ %(font_url("font.woff")) => %(http://www.example.com/fonts/font.woff),
+ %(font_url("font.ttf")) => %(http://www.example.com/fonts/font.ttf),
+ %(font_url("font.ttf?123")) => %(http://www.example.com/fonts/font.ttf?123),
+ %(font_url("font.ttf", host: "http://assets.example.com")) => %(http://assets.example.com/fonts/font.ttf)
+ }
+
+ UrlToFontToTag = {
+ %(url_to_font("font.eot")) => %(http://www.example.com/fonts/font.eot),
+ %(url_to_font("font.eot#iefix")) => %(http://www.example.com/fonts/font.eot#iefix),
+ %(url_to_font("font.woff")) => %(http://www.example.com/fonts/font.woff),
+ %(url_to_font("font.ttf")) => %(http://www.example.com/fonts/font.ttf),
+ %(url_to_font("font.ttf?123")) => %(http://www.example.com/fonts/font.ttf?123),
+ %(url_to_font("font.ttf", host: "http://assets.example.com")) => %(http://assets.example.com/fonts/font.ttf)
+ }
+
def test_autodiscovery_link_tag_with_unknown_type_but_not_pass_type_option_key
assert_raise(ArgumentError) do
auto_discovery_link_tag(:xml)
@@ -377,7 +411,7 @@ class AssetTagHelperTest < ActionView::TestCase
end
def test_javascript_include_tag_is_html_safe
- assert javascript_include_tag("prototype").html_safe?
+ assert_predicate javascript_include_tag("prototype"), :html_safe?
end
def test_javascript_include_tag_relative_protocol
@@ -391,6 +425,10 @@ class AssetTagHelperTest < ActionView::TestCase
assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag("prototype")
end
+ def test_javascript_include_tag_nonce
+ assert_dom_equal %(<script src="/javascripts/bank.js" nonce="iyhD0Yc0W+c="></script>), javascript_include_tag("bank", nonce: true)
+ end
+
def test_stylesheet_path
StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
@@ -421,9 +459,17 @@ class AssetTagHelperTest < ActionView::TestCase
}
end
+ def test_stylesheet_link_tag_without_request
+ @request = nil
+ assert_dom_equal(
+ %(<link rel="stylesheet" media="screen" href="/stylesheets/foo.css" />),
+ stylesheet_link_tag("foo.css")
+ )
+ end
+
def test_stylesheet_link_tag_is_html_safe
- assert stylesheet_link_tag("dir/file").html_safe?
- assert stylesheet_link_tag("dir/other/file", "dir/file2").html_safe?
+ assert_predicate stylesheet_link_tag("dir/file"), :html_safe?
+ assert_predicate stylesheet_link_tag("dir/other/file", "dir/file2"), :html_safe?
end
def test_stylesheet_link_tag_escapes_options
@@ -445,6 +491,11 @@ class AssetTagHelperTest < ActionView::TestCase
assert_dom_equal %(<link href="//assets.example.com/stylesheets/wellington.css" media="screen" rel="stylesheet" />), stylesheet_link_tag("wellington")
end
+ def test_javascript_include_tag_without_request
+ @request = nil
+ assert_dom_equal %(<script src="/javascripts/foo.js"></script>), javascript_include_tag("foo.js")
+ end
+
def test_image_path
ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
@@ -503,6 +554,10 @@ class AssetTagHelperTest < ActionView::TestCase
FaviconLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
+ def test_preload_link_tag
+ PreloadLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
def test_video_path
VideoPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
@@ -547,6 +602,14 @@ class AssetTagHelperTest < ActionView::TestCase
FontPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
+ def test_font_url
+ FontUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_url_to_font_alias_for_font_url
+ UrlToFontToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
def test_video_audio_tag_does_not_modify_options
options = { autoplay: true }
video_tag("video", options)
@@ -627,7 +690,9 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase
@controller = BasicController.new
@controller.config.relative_url_root = "/collaboration/hieraki"
- @request = Struct.new(:protocol, :base_url).new("gopher://", "gopher://www.example.com")
+ @request = Struct.new(:protocol, :base_url) do
+ def send_early_hints(links); end
+ end.new("gopher://", "gopher://www.example.com")
@controller.request = @request
end
@@ -749,6 +814,23 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase
end
end
+class AssetTagHelperWithoutRequestTest < ActionView::TestCase
+ tests ActionView::Helpers::AssetTagHelper
+
+ undef :request
+
+ def test_stylesheet_link_tag_without_request
+ assert_dom_equal(
+ %(<link rel="stylesheet" media="screen" href="/stylesheets/foo.css" />),
+ stylesheet_link_tag("foo.css")
+ )
+ end
+
+ def test_javascript_include_tag_without_request
+ assert_dom_equal %(<script src="/javascripts/foo.js"></script>), javascript_include_tag("foo.js")
+ end
+end
+
class AssetUrlHelperControllerTest < ActionView::TestCase
tests ActionView::Helpers::AssetUrlHelper
diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb
index 1be20dcaae..8e683cb48a 100644
--- a/actionview/test/template/atom_feed_helper_test.rb
+++ b/actionview/test/template/atom_feed_helper_test.rb
@@ -257,7 +257,7 @@ class AtomFeedTest < ActionController::TestCase
get :index, params: { id: "provide_builder" }
# because we pass in the non-default builder, the content generated by the
# helper should go 'nowhere'. Leaving the response body blank.
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
end
end
diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb
index 8a1c00fd00..131e49327e 100644
--- a/actionview/test/template/capture_helper_test.rb
+++ b/actionview/test/template/capture_helper_test.rb
@@ -49,21 +49,21 @@ class CaptureHelperTest < ActionView::TestCase
end
def test_content_for_with_multiple_calls
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title, "foo"
content_for :title, "bar"
assert_equal "foobar", content_for(:title)
end
def test_content_for_with_multiple_calls_and_flush
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title, "foo"
content_for :title, "bar", flush: true
assert_equal "bar", content_for(:title)
end
def test_content_for_with_block
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title do
output_buffer << "foo"
output_buffer << "bar"
@@ -73,7 +73,7 @@ class CaptureHelperTest < ActionView::TestCase
end
def test_content_for_with_block_and_multiple_calls_with_flush
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title do
"foo"
end
@@ -84,7 +84,7 @@ class CaptureHelperTest < ActionView::TestCase
end
def test_content_for_with_block_and_multiple_calls_with_flush_nil_content
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title do
"foo"
end
@@ -95,7 +95,7 @@ class CaptureHelperTest < ActionView::TestCase
end
def test_content_for_with_block_and_multiple_calls_without_flush
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title do
"foo"
end
@@ -106,7 +106,7 @@ class CaptureHelperTest < ActionView::TestCase
end
def test_content_for_with_whitespace_block
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title, "foo"
content_for :title do
output_buffer << " \n "
@@ -117,7 +117,7 @@ class CaptureHelperTest < ActionView::TestCase
end
def test_content_for_with_whitespace_block_and_flush
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title, "foo"
content_for :title, flush: true do
output_buffer << " \n "
@@ -128,7 +128,7 @@ class CaptureHelperTest < ActionView::TestCase
end
def test_content_for_returns_nil_when_writing
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
assert_nil content_for(:title, "foo")
assert_nil content_for(:title) { output_buffer << "bar"; nil }
assert_nil content_for(:title) { output_buffer << " \n "; nil }
@@ -144,27 +144,27 @@ class CaptureHelperTest < ActionView::TestCase
end
def test_content_for_question_mark
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title, "title"
assert content_for?(:title)
- assert ! content_for?(:something_else)
+ assert_not content_for?(:something_else)
end
def test_content_for_should_be_html_safe_after_flush_empty
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title do
content_tag(:p, "title")
end
- assert content_for(:title).html_safe?
+ assert_predicate content_for(:title), :html_safe?
content_for :title, "", flush: true
content_for(:title) do
content_tag(:p, "title")
end
- assert content_for(:title).html_safe?
+ assert_predicate content_for(:title), :html_safe?
end
def test_provide
- assert !content_for?(:title)
+ assert_not content_for?(:title)
provide :title, "hi"
assert content_for?(:title)
assert_equal "hi", content_for(:title)
diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb
index 5a5550438b..4b4939d705 100644
--- a/actionview/test/template/date_helper_test.rb
+++ b/actionview/test/template/date_helper_test.rb
@@ -141,18 +141,16 @@ class DateHelperTest < ActionView::TestCase
end
def test_distance_in_words_doesnt_use_the_quotient_operator
- rubinius_skip "Date is written in Ruby and relies on Fixnum#/"
- jruby_skip "Date is written in Ruby and relies on Fixnum#/"
+ rubinius_skip "Date is written in Ruby and relies on Integer#/"
+ jruby_skip "Date is written in Ruby and relies on Integer#/"
- klass = RUBY_VERSION > "2.4" ? Integer : Fixnum
-
- # Make sure that we avoid {Integer,Fixnum}#/ (redefined by mathn)
- klass.send :private, :/
+ # Make sure that we avoid Integer#/ (redefined by mathn)
+ Integer.send :private, :/
from = Time.utc(2004, 6, 6, 21, 45, 0)
assert_distance_of_time_in_words(from)
ensure
- klass.send :public, :/
+ Integer.send :public, :/
end
def test_time_ago_in_words_passes_include_seconds
@@ -687,7 +685,7 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="15">15</option>\n<option value="30">30</option>\n<option value="45">45</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), include_blank: true , minute_step: 15)
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), include_blank: true, minute_step: 15)
end
def test_select_minute_nil_with_blank
@@ -703,7 +701,7 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="15">15</option>\n<option value="30">30</option>\n<option value="45">45</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_minute(nil, include_blank: true , minute_step: 15)
+ assert_dom_equal expected, select_minute(nil, include_blank: true, minute_step: 15)
end
def test_select_minute_with_hidden
@@ -3593,25 +3591,25 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_html_safety
- assert select_day(16).html_safe?
- assert select_month(8).html_safe?
- assert select_year(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe?
- assert select_minute(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe?
- assert select_second(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe?
+ assert_predicate select_day(16), :html_safe?
+ assert_predicate select_month(8), :html_safe?
+ assert_predicate select_year(Time.mktime(2003, 8, 16, 8, 4, 18)), :html_safe?
+ assert_predicate select_minute(Time.mktime(2003, 8, 16, 8, 4, 18)), :html_safe?
+ assert_predicate select_second(Time.mktime(2003, 8, 16, 8, 4, 18)), :html_safe?
- assert select_minute(8, use_hidden: true).html_safe?
- assert select_month(8, prompt: "Choose month").html_safe?
+ assert_predicate select_minute(8, use_hidden: true), :html_safe?
+ assert_predicate select_month(8, prompt: "Choose month"), :html_safe?
- assert select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, { class: "selector" }).html_safe?
- assert select_date(Time.mktime(2003, 8, 16), date_separator: " / ", start_year: 2003, end_year: 2005, prefix: "date[first]").html_safe?
+ assert_predicate select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, { class: "selector" }), :html_safe?
+ assert_predicate select_date(Time.mktime(2003, 8, 16), date_separator: " / ", start_year: 2003, end_year: 2005, prefix: "date[first]"), :html_safe?
end
def test_object_select_html_safety
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- assert date_select("post", "written_on", default: Time.local(2006, 9, 19, 15, 16, 35), include_blank: true).html_safe?
- assert time_select("post", "written_on", ignore_date: true).html_safe?
+ assert_predicate date_select("post", "written_on", default: Time.local(2006, 9, 19, 15, 16, 35), include_blank: true), :html_safe?
+ assert_predicate time_select("post", "written_on", ignore_date: true), :html_safe?
end
def test_time_tag_with_date
diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb
index 928b1ac7dd..ddaa7febb3 100644
--- a/actionview/test/template/digestor_test.rb
+++ b/actionview/test/template/digestor_test.rb
@@ -4,17 +4,6 @@ require "abstract_unit"
require "fileutils"
require "action_view/dependency_tracker"
-class FixtureTemplate
- attr_reader :source, :handler
-
- def initialize(template_path)
- @source = File.read(template_path)
- @handler = ActionView::Template.handler_for_extension(:erb)
- rescue Errno::ENOENT
- raise ActionView::MissingTemplate.new([], "", [], true, [])
- end
-end
-
class FixtureFinder < ActionView::LookupContext
FIXTURES_DIR = File.expand_path("../fixtures/digestor", __dir__)
@@ -171,6 +160,18 @@ class TemplateDigestorTest < ActionView::TestCase
assert_equal [:html], tree_template_formats("messages/show").uniq
end
+ def test_template_dependencies_with_fallback_from_js_to_html_format
+ finder.rendered_format = :js
+ assert_equal ["comments/comment"], dependencies("comments/show")
+ end
+
+ def test_template_digest_with_fallback_from_js_to_html_format
+ finder.rendered_format = :js
+ assert_digest_difference("comments/show") do
+ change_template("comments/_comment")
+ end
+ end
+
def test_recursion_in_renders
assert digest("level/recursion") # assert recursion is possible
assert_not_nil digest("level/recursion") # assert digest is stored
diff --git a/actionview/test/template/erb/deprecated_erubis_implementation_test.rb b/actionview/test/template/erb/deprecated_erubis_implementation_test.rb
deleted file mode 100644
index ea088e7cfc..0000000000
--- a/actionview/test/template/erb/deprecated_erubis_implementation_test.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-require "abstract_unit"
-
-module ERBTest
- class DeprecatedErubisImplementationTest < ActionView::TestCase
- test "Erubis implementation is deprecated" do
- assert_deprecated "ActionView::Template::Handlers::Erubis is deprecated and will be removed from Rails 5.2. Switch to ActionView::Template::Handlers::ERB::Erubi instead." do
- assert_equal "ActionView::Template::Handlers::ERB::Erubis", ActionView::Template::Handlers::Erubis.to_s
-
- assert_nothing_raised { Class.new(ActionView::Template::Handlers::Erubis) }
- end
- end
- end
-end
diff --git a/actionview/test/template/erb_util_test.rb b/actionview/test/template/erb_util_test.rb
index 8b804105f4..bd702dbe94 100644
--- a/actionview/test/template/erb_util_test.rb
+++ b/actionview/test/template/erb_util_test.rb
@@ -70,24 +70,24 @@ class ErbUtilTest < ActiveSupport::TestCase
def test_json_escape_returns_unsafe_strings_when_passed_unsafe_strings
value = json_escape("asdf")
- assert !value.html_safe?
+ assert_not_predicate value, :html_safe?
end
def test_json_escape_returns_safe_strings_when_passed_safe_strings
value = json_escape("asdf".html_safe)
- assert value.html_safe?
+ assert_predicate value, :html_safe?
end
def test_html_escape_is_html_safe
escaped = h("<p>")
assert_equal "&lt;p&gt;", escaped
- assert escaped.html_safe?
+ assert_predicate escaped, :html_safe?
end
def test_html_escape_passes_html_escape_unmodified
escaped = h("<p>".html_safe)
assert_equal "<p>", escaped
- assert escaped.html_safe?
+ assert_predicate escaped, :html_safe?
end
def test_rest_in_ascii
@@ -104,11 +104,11 @@ class ErbUtilTest < ActiveSupport::TestCase
def test_html_escape_once_returns_unsafe_strings_when_passed_unsafe_strings
value = html_escape_once("1 < 2 &amp; 3")
- assert !value.html_safe?
+ assert_not_predicate value, :html_safe?
end
def test_html_escape_once_returns_safe_strings_when_passed_safe_strings
value = html_escape_once("1 < 2 &amp; 3".html_safe)
- assert value.html_safe?
+ assert_predicate value, :html_safe?
end
end
diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb
index c7d49070ce..6b65d740eb 100644
--- a/actionview/test/template/form_helper/form_with_test.rb
+++ b/actionview/test/template/form_helper/form_with_test.rb
@@ -5,6 +5,25 @@ require "controller/fake_models"
class FormWithTest < ActionView::TestCase
include RenderERBUtils
+
+ setup do
+ @old_value = ActionView::Helpers::FormHelper.form_with_generates_ids
+ ActionView::Helpers::FormHelper.form_with_generates_ids = true
+ end
+
+ teardown do
+ ActionView::Helpers::FormHelper.form_with_generates_ids = @old_value
+ end
+
+ private
+ def with_default_enforce_utf8(value)
+ old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value
+
+ yield
+ ensure
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value
+ end
end
class FormWithActsLikeFormTagTest < FormWithTest
@@ -99,7 +118,25 @@ class FormWithActsLikeFormTagTest < FormWithTest
actual = form_with(skip_enforcing_utf8: true)
expected = whole_form("http://www.example.com", skip_enforcing_utf8: true)
assert_dom_equal expected, actual
- assert actual.html_safe?
+ assert_predicate actual, :html_safe?
+ end
+
+ def test_form_with_default_enforce_utf8_false
+ with_default_enforce_utf8 false do
+ actual = form_with
+ expected = whole_form("http://www.example.com", skip_enforcing_utf8: true)
+ assert_dom_equal expected, actual
+ assert_predicate actual, :html_safe?
+ end
+ end
+
+ def test_form_with_default_enforce_utf8_true
+ with_default_enforce_utf8 true do
+ actual = form_with
+ expected = whole_form("http://www.example.com", skip_enforcing_utf8: false)
+ assert_dom_equal expected, actual
+ assert_predicate actual, :html_safe?
+ end
end
def test_form_with_with_block_in_erb
@@ -181,6 +218,9 @@ class FormWithActsLikeFormForTest < FormWithTest
submit: "Save changes",
another_post: {
update: "Update your %{model}"
+ },
+ "blog/post": {
+ update: "Update your %{model}"
}
}
}
@@ -218,7 +258,7 @@ class FormWithActsLikeFormForTest < FormWithTest
@post = Post.new
@comment = Comment.new
- def @post.errors()
+ def @post.errors
Class.new {
def [](field); field == "author_name" ? ["can't be empty"] : [] end
def empty?() false end
@@ -314,17 +354,45 @@ class FormWithActsLikeFormForTest < FormWithTest
expected = whole_form("/posts/123", "create-post", method: "patch") do
"<label for='post_title'>The Title</label>" \
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />" \
+ "<select name='post[category]' id='post_category'><option value='animal'>animal</option>\n<option value='economy'>economy</option>\n<option value='sports'>sports</option></select>" \
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" \
+ "<button name='button' type='submit'>Create post</button>" \
+ "<button name='button' type='submit'><span>Create post</span></button>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_not_outputting_ids
+ old_value = ActionView::Helpers::FormHelper.form_with_generates_ids
+ ActionView::Helpers::FormHelper.form_with_generates_ids = false
+
+ form_with(model: @post, id: "create-post") do |f|
+ concat f.label(:title) { "The Title" }
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ concat f.select(:category, %w( animal economy sports ))
+ concat f.submit("Create post")
+ end
+
+ expected = whole_form("/posts/123", "create-post", method: "patch") do
+ "<label>The Title</label>" \
"<input name='post[title]' type='text' value='Hello World' />" \
"<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \
"<input name='post[secret]' type='hidden' value='0' />" \
"<input name='post[secret]' checked='checked' type='checkbox' value='1' />" \
"<select name='post[category]'><option value='animal'>animal</option>\n<option value='economy'>economy</option>\n<option value='sports'>sports</option></select>" \
- "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" \
- "<button name='button' type='submit'>Create post</button>" \
- "<button name='button' type='submit'><span>Create post</span></button>"
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />"
end
assert_dom_equal expected, output_buffer
+ ensure
+ ActionView::Helpers::FormHelper.form_with_generates_ids = old_value
end
def test_form_with_only_url_on_create
@@ -335,7 +403,7 @@ class FormWithActsLikeFormForTest < FormWithTest
expected = whole_form("/posts") do
'<label for="title">Label me</label>' \
- '<input type="text" name="title">'
+ '<input type="text" name="title" id="title">'
end
assert_dom_equal expected, output_buffer
@@ -349,7 +417,7 @@ class FormWithActsLikeFormForTest < FormWithTest
expected = whole_form("/posts/123") do
'<label for="title">Label me</label>' \
- '<input type="text" name="title">'
+ '<input type="text" name="title" id="title">'
end
assert_dom_equal expected, output_buffer
@@ -361,7 +429,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123") do
- '<input type="text" name="no_model_to_back_this_badboy">'
+ '<input type="text" name="no_model_to_back_this_badboy" id="no_model_to_back_this_badboy" >'
end
assert_dom_equal expected, output_buffer
@@ -373,7 +441,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: :patch) do
- '<input type="text" name="post[this_dont_exist_on_post]">'
+ '<input type="text" name="post[this_dont_exist_on_post]" id="post_this_dont_exist_on_post" >'
end
assert_dom_equal expected, output_buffer
@@ -391,8 +459,8 @@ class FormWithActsLikeFormForTest < FormWithTest
end.new
form_with(model: obj, scope: "other_name", url: "/", id: "edit-other-name") do |f|
- assert_dom_equal '<input type="hidden" name="other_name[private_property]">', f.hidden_field(:private_property)
- assert_dom_equal '<input type="hidden" name="other_name[protected_property]">', f.hidden_field(:protected_property)
+ assert_dom_equal '<input type="hidden" name="other_name[private_property]" id="other_name_private_property">', f.hidden_field(:private_property)
+ assert_dom_equal '<input type="hidden" name="other_name[protected_property]" id="other_name_protected_property">', f.hidden_field(:protected_property)
end
end
@@ -459,7 +527,7 @@ class FormWithActsLikeFormForTest < FormWithTest
"<label for='post_active_false'>" \
"<input checked='checked' name='post[active]' type='radio' value='false' id='post_active_false' />" \
"false</label>" \
- "<input name='post[id]' type='hidden' value='1' />"
+ "<input name='post[id]' type='hidden' value='1' id='post_id' />"
end
assert_dom_equal expected, output_buffer
@@ -557,7 +625,7 @@ class FormWithActsLikeFormForTest < FormWithTest
"<label for='post_tag_ids_3'>" \
"<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' id='post_tag_ids_3' />" \
"Tag 3</label>" \
- "<input name='post[id]' type='hidden' value='1' />"
+ "<input name='post[id]' type='hidden' value='1' id='post_id' />"
end
assert_dom_equal expected, output_buffer
@@ -587,7 +655,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", "create-post", method: "patch", multipart: true) do
- "<input name='post[file]' type='file' />"
+ "<input name='post[file]' type='file' id='post_file' />"
end
assert_dom_equal expected, output_buffer
@@ -601,7 +669,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch", multipart: true) do
- "<input name='post[comment][file]' type='file' />"
+ "<input name='post[comment][file]' type='file' id='post_comment_file'/>"
end
assert_dom_equal expected, output_buffer
@@ -640,7 +708,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/44", method: "patch") do
- "<input name='post[title]' type='text' value='And his name will be forty and four.' />" \
+ "<input name='post[title]' type='text' value='And his name will be forty and four.' id='post_title' />" \
"<input name='commit' data-disable-with='Edit post' type='submit' value='Edit post' />"
end
@@ -658,10 +726,10 @@ class FormWithActsLikeFormForTest < FormWithTest
expected = whole_form("/posts/123", "create-post", method: "patch") do
"<label for='other_name_title' class='post_title'>Title</label>" \
- "<input name='other_name[title]' value='Hello World' type='text' />" \
- "<textarea name='other_name[body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='other_name[title]' value='Hello World' type='text' id='other_name_title' />" \
+ "<textarea name='other_name[body]' id='other_name_body'>\nBack to the hill and over it again!</textarea>" \
"<input name='other_name[secret]' value='0' type='hidden' />" \
- "<input name='other_name[secret]' checked='checked' value='1' type='checkbox' />" \
+ "<input name='other_name[secret]' checked='checked' value='1' type='checkbox' id='other_name_secret' />" \
"<input name='commit' value='Create post' data-disable-with='Create post' type='submit' />"
end
@@ -676,10 +744,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/", "create-post", method: "delete") do
- "<input name='post[title]' type='text' value='Hello World' />" \
- "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
"<input name='post[secret]' type='hidden' value='0' />" \
- "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret'/>"
end
assert_dom_equal expected, output_buffer
@@ -693,10 +761,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/", "create-post", method: "delete") do
- "<input name='post[title]' type='text' value='Hello World' />" \
- "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \
"<input name='post[secret]' type='hidden' value='0' />" \
- "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
end
assert_dom_equal expected, output_buffer
@@ -710,7 +778,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/search", "search-post", method: "get") do
- "<input name='post[title]' type='search' />"
+ "<input name='post[title]' type='search' id='post_title' />"
end
assert_dom_equal expected, output_buffer
@@ -724,10 +792,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/", "create-post", method: "patch") do
- "<input name='post[title]' type='text' value='Hello World' />" \
- "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \
"<input name='post[secret]' type='hidden' value='0' />" \
- "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
end
assert_dom_equal expected, output_buffer
@@ -744,10 +812,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/", "create-post", method: "patch", local: true) do
- "<input name='post[title]' type='text' value='Hello World' />" \
- "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
"<input name='post[secret]' type='hidden' value='0' />" \
- "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
end
assert_dom_equal expected, output_buffer
@@ -761,7 +829,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/", skip_enforcing_utf8: true) do
- "<input name='post[title]' type='text' value='Hello World' />"
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />"
end
assert_dom_equal expected, output_buffer
@@ -773,12 +841,40 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/", skip_enforcing_utf8: false) do
- "<input name='post[title]' type='text' value='Hello World' />"
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />"
end
assert_dom_equal expected, output_buffer
end
+ def test_form_with_default_enforce_utf8_true
+ with_default_enforce_utf8 true do
+ form_with(scope: :post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", skip_enforcing_utf8: false) do
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_form_with_default_enforce_utf8_false
+ with_default_enforce_utf8 false do
+ form_with(scope: :post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", skip_enforcing_utf8: true) do
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
def test_form_with_without_object
form_with(scope: :post, id: "create-post") do |f|
concat f.text_field(:title)
@@ -787,10 +883,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/", "create-post") do
- "<input name='post[title]' type='text' value='Hello World' />" \
- "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \
"<input name='post[secret]' type='hidden' value='0' />" \
- "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
end
assert_dom_equal expected, output_buffer
@@ -806,10 +902,10 @@ class FormWithActsLikeFormForTest < FormWithTest
expected = whole_form("/posts/123", method: "patch") do
"<label for='post_123_title'>Title</label>" \
- "<input name='post[123][title]' type='text' value='Hello World' />" \
- "<textarea name='post[123][body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[123][title]' type='text' value='Hello World' id='post_123_title' />" \
+ "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" \
"<input name='post[123][secret]' type='hidden' value='0' />" \
- "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' id='post_123_secret' />"
end
assert_dom_equal expected, output_buffer
@@ -823,10 +919,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<input name='post[][title]' type='text' value='Hello World' />" \
- "<textarea name='post[][body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[][title]' type='text' value='Hello World' id='post__title' />" \
+ "<textarea name='post[][body]' id='post__body' >\nBack to the hill and over it again!</textarea>" \
"<input name='post[][secret]' type='hidden' value='0' />" \
- "<input name='post[][secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='post[][secret]' checked='checked' type='checkbox' value='1' id='post__secret' />"
end
assert_dom_equal expected, output_buffer
@@ -841,7 +937,7 @@ class FormWithActsLikeFormForTest < FormWithTest
expected = whole_form("/posts/123", method: "patch") do
"<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" \
- "<div class='field_with_errors'><input name='post[author_name]' type='text' value='' /></div>" \
+ "<div class='field_with_errors'><input name='post[author_name]' type='text' value='' id='post_author_name' /></div>" \
"<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />"
end
@@ -859,7 +955,7 @@ class FormWithActsLikeFormForTest < FormWithTest
expected = whole_form("/posts/123", method: "patch") do
"<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" \
- "<div class='field_with_errors'><input name='post[author_name]' type='text' value='' /></div>" \
+ "<div class='field_with_errors'><input name='post[author_name]' type='text' value='' id='post_author_name' /></div>" \
"<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />"
end
@@ -925,7 +1021,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
end
- def test_submit_with_object_and_nested_lookup
+ def test_submit_with_object_which_is_overwritten_by_scope_option
with_locale :submit do
form_with(model: @post, scope: :another_post) do |f|
concat f.submit
@@ -939,6 +1035,21 @@ class FormWithActsLikeFormForTest < FormWithTest
end
end
+ def test_submit_with_object_which_is_namespaced
+ blog_post = Blog::Post.new("And his name will be forty and four.", 44)
+ with_locale :submit do
+ form_with(model: blog_post) do |f|
+ concat f.submit
+ end
+
+ expected = whole_form("/posts/44", method: "patch") do
+ "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
def test_fields_with_attributes_not_on_model
form_with(model: @post) do |f|
concat f.fields(:comment) { |c|
@@ -947,7 +1058,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: :patch) do
- '<input type="text" name="post[comment][dont_exist_on_model]">'
+ '<input type="text" name="post[comment][dont_exist_on_model]" id="post_comment_dont_exist_on_model" >'
end
assert_dom_equal expected, output_buffer
@@ -967,7 +1078,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form do
- '<input name="posts[post][0][comment][1][dont_exist_on_model]" type="text">'
+ '<input name="posts[post][0][comment][1][dont_exist_on_model]" type="text" id="posts_post_0_comment_1_dont_exist_on_model" >'
end
assert_dom_equal expected, output_buffer
@@ -982,7 +1093,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<input name='post[comment][body]' type='text' value='Hello World' />"
+ "<input name='post[comment][body]' type='text' value='Hello World' id='post_comment_body' />"
end
assert_dom_equal expected, output_buffer
@@ -1002,7 +1113,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form do
- "<input name='posts[post][0][comment][1][name]' type='text' value='comment #1' />"
+ "<input name='posts[post][0][comment][1][name]' type='text' value='comment #1' id='posts_post_0_comment_1_name' />"
end
assert_dom_equal expected, output_buffer
@@ -1017,8 +1128,8 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<input name='post[123][title]' type='text' value='Hello World' />" \
- "<input name='post[123][comment][][name]' type='text' value='new comment' />"
+ "<input name='post[123][title]' type='text' value='Hello World' id='post_123_title' />" \
+ "<input name='post[123][comment][][name]' type='text' value='new comment' id='post_123_comment__name' />"
end
assert_dom_equal expected, output_buffer
@@ -1033,8 +1144,8 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<input name='post[1][title]' type='text' value='Hello World' />" \
- "<input name='post[1][comment][1][name]' type='text' value='new comment' />"
+ "<input name='post[1][title]' type='text' value='Hello World' id='post_1_title' />" \
+ "<input name='post[1][comment][1][name]' type='text' value='new comment' id='post_1_comment_1_name' />"
end
assert_dom_equal expected, output_buffer
@@ -1048,7 +1159,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<input name='post[1][comment][title]' type='text' value='Hello World' />"
+ "<input name='post[1][comment][title]' type='text' value='Hello World' id='post_1_comment_title' />"
end
assert_dom_equal expected, output_buffer
@@ -1062,7 +1173,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<input name='post[1][comment][5][title]' type='text' value='Hello World' />"
+ "<input name='post[1][comment][5][title]' type='text' value='Hello World' id='post_1_comment_5_title' />"
end
assert_dom_equal expected, output_buffer
@@ -1076,7 +1187,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<input name='post[123][comment][title]' type='text' value='Hello World' />"
+ "<input name='post[123][comment][title]' type='text' value='Hello World' id='post_123_comment_title' />"
end
assert_dom_equal expected, output_buffer
@@ -1090,7 +1201,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<input name='post[comment][5][title]' type='radio' value='hello' />"
+ "<input name='post[comment][5][title]' type='radio' value='hello' id='post_comment_5_title_hello' />"
end
assert_dom_equal expected, output_buffer
@@ -1104,7 +1215,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<input name='post[123][comment][123][title]' type='text' value='Hello World' />"
+ "<input name='post[123][comment][123][title]' type='text' value='Hello World' id='post_123_comment_123_title' />"
end
assert_dom_equal expected, output_buffer
@@ -1124,9 +1235,9 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<input name='post[123][comment][5][title]' type='text' value='Hello World' />"
+ "<input name='post[123][comment][5][title]' type='text' value='Hello World' id='post_123_comment_5_title' />"
end + whole_form("/posts/123", method: "patch") do
- "<input name='post[1][comment][123][title]' type='text' value='Hello World' />"
+ "<input name='post[1][comment][123][title]' type='text' value='Hello World' id='post_1_comment_123_title' />"
end
assert_dom_equal expected, output_buffer
@@ -1143,8 +1254,8 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[author_attributes][name]" type="text" value="new author" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="new author" id="post_author_attributes_name" />'
end
assert_dom_equal expected, output_buffer
@@ -1170,9 +1281,9 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[author_attributes][name]" type="text" value="author #321" />' \
- '<input name="post[author_attributes][id]" type="hidden" value="321" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \
+ '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />'
end
assert_dom_equal expected, output_buffer
@@ -1189,9 +1300,9 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[author_attributes][name]" type="text" value="author #321" />' \
- '<input name="post[author_attributes][id]" type="hidden" value="321" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \
+ '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />'
end
assert_dom_equal expected, output_buffer
@@ -1208,8 +1319,8 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[author_attributes][name]" type="text" value="author #321" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />'
end
assert_dom_equal expected, output_buffer
@@ -1226,8 +1337,8 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[author_attributes][name]" type="text" value="author #321" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />'
end
assert_dom_equal expected, output_buffer
@@ -1244,9 +1355,9 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[author_attributes][name]" type="text" value="author #321" />' \
- '<input name="post[author_attributes][id]" type="hidden" value="321" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \
+ '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />'
end
assert_dom_equal expected, output_buffer
@@ -1264,9 +1375,9 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[author_attributes][id]" type="hidden" value="321" />' \
- '<input name="post[author_attributes][name]" type="text" value="author #321" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />'
end
assert_dom_equal expected, output_buffer
@@ -1285,11 +1396,11 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
- '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \
- '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \
- '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \
+ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />'
end
assert_dom_equal expected, output_buffer
@@ -1312,11 +1423,11 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[author_attributes][name]" type="text" value="author #321" />' \
- '<input name="post[author_attributes][id]" type="hidden" value="321" />' \
- '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
- '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \
+ '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />'
end
assert_dom_equal expected, output_buffer
@@ -1339,10 +1450,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[author_attributes][name]" type="text" value="author #321" />' \
- '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
- '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />'
end
assert_dom_equal expected, output_buffer
@@ -1365,11 +1476,11 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[author_attributes][name]" type="text" value="author #321" />' \
- '<input name="post[author_attributes][id]" type="hidden" value="321" />' \
- '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
- '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \
+ '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />'
end
assert_dom_equal expected, output_buffer
@@ -1388,11 +1499,11 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
- '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \
- '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \
- '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \
+ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />'
end
assert_dom_equal expected, output_buffer
@@ -1412,11 +1523,11 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \
- '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
- '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' \
- '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />'
end
assert_dom_equal expected, output_buffer
@@ -1435,9 +1546,9 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[comments_attributes][0][name]" type="text" value="new comment" />' \
- '<input name="post[comments_attributes][1][name]" type="text" value="new comment" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="new comment" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="new comment" id="post_comments_attributes_1_name" />'
end
assert_dom_equal expected, output_buffer
@@ -1456,10 +1567,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \
- '<input name="post[comments_attributes][0][id]" type="hidden" value="321" />' \
- '<input name="post[comments_attributes][1][name]" type="text" value="new comment" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="321" id="post_comments_attributes_0_id"/>' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="new comment" id="post_comments_attributes_1_name" />'
end
assert_dom_equal expected, output_buffer
@@ -1474,7 +1585,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />'
end
assert_dom_equal expected, output_buffer
@@ -1491,11 +1602,11 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
- '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \
- '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \
- '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \
+ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />'
end
assert_dom_equal expected, output_buffer
@@ -1512,11 +1623,11 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
- '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \
- '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \
- '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \
+ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />'
end
assert_dom_equal expected, output_buffer
@@ -1547,11 +1658,11 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
- '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \
- '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \
- '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \
+ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />'
end
assert_dom_equal expected, output_buffer
@@ -1570,10 +1681,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[title]" type="text" value="Hello World" />' \
- '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \
- '<input name="post[comments_attributes][0][id]" type="hidden" value="321" />' \
- '<input name="post[comments_attributes][1][name]" type="text" value="new comment" />'
+ '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="321" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="new comment" id="post_comments_attributes_1_name" />'
end
assert_dom_equal expected, output_buffer
@@ -1590,8 +1701,8 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \
- '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" />'
+ '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" id="post_comments_attributes_abc_name" />' \
+ '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" id="post_comments_attributes_abc_id" />'
end
assert_dom_equal expected, output_buffer
@@ -1607,8 +1718,8 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \
- '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" />'
+ '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" id="post_comments_attributes_abc_name" />' \
+ '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" id="post_comments_attributes_abc_id" />'
end
assert_dom_equal expected, output_buffer
@@ -1630,8 +1741,8 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \
- '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" />'
+ '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" id="post_comments_attributes_abc_name" />' \
+ '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" id="post_comments_attributes_abc_id" />'
end
assert_dom_equal expected, output_buffer
@@ -1716,18 +1827,18 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \
- '<input name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" />' \
- '<input name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' \
- '<input name="post[comments_attributes][0][id]" type="hidden" value="321" />' \
- '<input name="post[tags_attributes][0][value]" type="text" value="tag #123" />' \
- '<input name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" />' \
- '<input name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' \
- '<input name="post[tags_attributes][0][id]" type="hidden" value="123" />' \
- '<input name="post[tags_attributes][1][value]" type="text" value="tag #456" />' \
- '<input name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" />' \
- '<input name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' \
- '<input name="post[tags_attributes][1][id]" type="hidden" value="456" />'
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" id="post_comments_attributes_0_relevances_attributes_0_value" />' \
+ '<input name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" id="post_comments_attributes_0_relevances_attributes_0_id"/>' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="321" id="post_comments_attributes_0_id"/>' \
+ '<input name="post[tags_attributes][0][value]" type="text" value="tag #123" id="post_tags_attributes_0_value"/>' \
+ '<input name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" id="post_tags_attributes_0_relevances_attributes_0_value"/>' \
+ '<input name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" id="post_tags_attributes_0_relevances_attributes_0_id"/>' \
+ '<input name="post[tags_attributes][0][id]" type="hidden" value="123" id="post_tags_attributes_0_id"/>' \
+ '<input name="post[tags_attributes][1][value]" type="text" value="tag #456" id="post_tags_attributes_1_value"/>' \
+ '<input name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" id="post_tags_attributes_1_relevances_attributes_0_value"/>' \
+ '<input name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" id="post_tags_attributes_1_relevances_attributes_0_id"/>' \
+ '<input name="post[tags_attributes][1][id]" type="hidden" value="456" id="post_tags_attributes_1_id"/>'
end
assert_dom_equal expected, output_buffer
@@ -1743,7 +1854,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- '<input name="post[author_attributes][name]" type="text" value="hash backed author" />'
+ '<input name="post[author_attributes][name]" type="text" value="hash backed author" id="post_author_attributes_name" />'
end
assert_dom_equal expected, output_buffer
@@ -1757,10 +1868,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected =
- "<input name='post[title]' type='text' value='Hello World' />" \
- "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
"<input name='post[secret]' type='hidden' value='0' />" \
- "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
assert_dom_equal expected, output_buffer
end
@@ -1773,10 +1884,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected =
- "<input name='post[123][title]' type='text' value='Hello World' />" \
- "<textarea name='post[123][body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[123][title]' type='text' value='Hello World' id='post_123_title' />" \
+ "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" \
"<input name='post[123][secret]' type='hidden' value='0' />" \
- "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' id='post_123_secret' />"
assert_dom_equal expected, output_buffer
end
@@ -1789,10 +1900,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected =
- "<input name='post[][title]' type='text' value='Hello World' />" \
- "<textarea name='post[][body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[][title]' type='text' value='Hello World' id='post__title' />" \
+ "<textarea name='post[][body]' id='post__body'>\nBack to the hill and over it again!</textarea>" \
"<input name='post[][secret]' type='hidden' value='0' />" \
- "<input name='post[][secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='post[][secret]' checked='checked' type='checkbox' value='1' id='post__secret' />"
assert_dom_equal expected, output_buffer
end
@@ -1805,10 +1916,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected =
- "<input name='post[abc][title]' type='text' value='Hello World' />" \
- "<textarea name='post[abc][body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[abc][title]' type='text' value='Hello World' id='post_abc_title' />" \
+ "<textarea name='post[abc][body]' id='post_abc_body'>\nBack to the hill and over it again!</textarea>" \
"<input name='post[abc][secret]' type='hidden' value='0' />" \
- "<input name='post[abc][secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='post[abc][secret]' checked='checked' type='checkbox' value='1' id='post_abc_secret' />"
assert_dom_equal expected, output_buffer
end
@@ -1821,10 +1932,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected =
- "<input name='post[title]' type='text' value='Hello World' />" \
- "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \
"<input name='post[secret]' type='hidden' value='0' />" \
- "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
assert_dom_equal expected, output_buffer
end
@@ -1837,10 +1948,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected =
- "<input name='post[title]' type='text' value='Hello World' />" \
- "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \
"<input name='post[secret]' type='hidden' value='0' />" \
- "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
assert_dom_equal expected, output_buffer
end
@@ -1852,7 +1963,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
assert_dom_equal "<label for=\"author_post_title\">Title</label>" \
- "<input name='author[post][title]' type='text' value='Hello World' />",
+ "<input name='author[post][title]' type='text' value='Hello World' id='author_post_title' id='author_post_1_title' />",
output_buffer
end
@@ -1863,7 +1974,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" \
- "<input name='author[post][1][title]' type='text' value='Hello World' />",
+ "<input name='author[post][1][title]' type='text' value='Hello World' id='author_post_1_title' />",
output_buffer
end
@@ -1882,10 +1993,10 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", "create-post", method: "patch") do
- "<input name='post[title]' type='text' value='Hello World' />" \
- "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \
"<input name='parent_post[secret]' type='hidden' value='0' />" \
- "<input name='parent_post[secret]' checked='checked' type='checkbox' value='1' />"
+ "<input name='parent_post[secret]' checked='checked' type='checkbox' value='1' id='parent_post_secret' />"
end
assert_dom_equal expected, output_buffer
@@ -1902,9 +2013,9 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", "create-post", method: "patch") do
- "<input name='post[title]' type='text' value='Hello World' />" \
- "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \
- "<input name='post[comment][name]' type='text' value='new comment' />"
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[comment][name]' type='text' value='new comment' id='post_comment_name' />"
end
assert_dom_equal expected, output_buffer
@@ -1918,7 +2029,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<input name='post[category][name]' type='text' />"
+ "<input name='post[category][name]' type='text' id='post_category_name' />"
end
assert_dom_equal expected, output_buffer
@@ -1942,9 +2053,9 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" \
- "<label for='body'>Body:</label> <textarea name='post[body]'>\nBack to the hill and over it again!</textarea><br/>" \
- "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' /><br/>"
+ "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' id='post_title'/><br/>" \
+ "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" \
+ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' /><br/>"
end
assert_dom_equal expected, output_buffer
@@ -1961,9 +2072,9 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" \
- "<label for='body'>Body:</label> <textarea name='post[body]'>\nBack to the hill and over it again!</textarea><br/>" \
- "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' /><br/>"
+ "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' id='post_title' /><br/>" \
+ "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" \
+ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' /><br/>"
end
assert_dom_equal expected, output_buffer
@@ -1980,7 +2091,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected = whole_form("/posts/123", method: "patch") do
- "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>"
+ "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' id='post_title' /><br/>"
end
assert_dom_equal expected, output_buffer
@@ -1995,7 +2106,7 @@ class FormWithActsLikeFormForTest < FormWithTest
concat f.text_field(:title)
end
- expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>"
+ expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' id='post_title' /><br/>"
assert_dom_equal expected, output_buffer
end
@@ -2007,7 +2118,7 @@ class FormWithActsLikeFormForTest < FormWithTest
concat f.text_field(:title)
end
- expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>"
+ expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' id='post_title' /><br/>"
assert_dom_equal expected, output_buffer
end
@@ -2020,9 +2131,9 @@ class FormWithActsLikeFormForTest < FormWithTest
end
expected =
- "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" \
- "<label for='body'>Body:</label> <textarea name='post[body]'>\nBack to the hill and over it again!</textarea><br/>" \
- "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' /><br/>"
+ "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' id='post_title'/><br/>" \
+ "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" \
+ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' /><br/>"
assert_dom_equal expected, output_buffer
end
@@ -2236,4 +2347,13 @@ class FormWithActsLikeFormForTest < FormWithTest
ensure
I18n.locale = old_locale
end
+
+ def with_default_enforce_utf8(value)
+ old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value
+
+ yield
+ ensure
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value
+ end
end
diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb
index ac64096908..5244204e42 100644
--- a/actionview/test/template/form_helper_test.rb
+++ b/actionview/test/template/form_helper_test.rb
@@ -68,6 +68,9 @@ class FormHelperTest < ActionView::TestCase
submit: "Save changes",
another_post: {
update: "Update your %{model}"
+ },
+ "blog/post": {
+ update: "Update your %{model}"
}
}
}
@@ -105,7 +108,7 @@ class FormHelperTest < ActionView::TestCase
@post = Post.new
@comment = Comment.new
- def @post.errors()
+ def @post.errors
Class.new {
def [](field); field == "author_name" ? ["can't be empty"] : [] end
def empty?() false end
@@ -612,7 +615,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_check_box_is_html_safe
- assert check_box("post", "secret").html_safe?
+ assert_predicate check_box("post", "secret"), :html_safe?
end
def test_check_box_checked_if_object_value_is_same_that_check_value
@@ -747,19 +750,19 @@ class FormHelperTest < ActionView::TestCase
end
def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_big_decimal
- @post.secret = BigDecimal.new(0)
+ @post.secret = BigDecimal(0)
assert_dom_equal(
'<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />',
check_box("post", "secret", {}, 0, 1)
)
- @post.secret = BigDecimal.new(1)
+ @post.secret = BigDecimal(1)
assert_dom_equal(
'<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
check_box("post", "secret", {}, 0, 1)
)
- @post.secret = BigDecimal.new(2.2, 1)
+ @post.secret = BigDecimal(2.2, 1)
assert_dom_equal(
'<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
check_box("post", "secret", {}, 0, 1)
@@ -775,7 +778,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_check_box_with_nil_unchecked_value_is_html_safe
- assert check_box("post", "secret", {}, "on", nil).html_safe?
+ assert_predicate check_box("post", "secret", {}, "on", nil), :html_safe?
end
def test_check_box_with_multiple_behavior
@@ -1560,6 +1563,38 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_form_for_is_not_affected_by_form_with_generates_ids
+ old_value = ActionView::Helpers::FormHelper.form_with_generates_ids
+ ActionView::Helpers::FormHelper.form_with_generates_ids = false
+
+ form_for(@post, html: { id: "create-post" }) do |f|
+ concat f.label(:title) { "The Title" }
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ concat f.submit("Create post")
+ concat f.button("Create post")
+ concat f.button {
+ concat content_tag(:span, "Create post")
+ }
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ "<label for='post_title'>The Title</label>" \
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" \
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" \
+ "<button name='button' type='submit'>Create post</button>" \
+ "<button name='button' type='submit'><span>Create post</span></button>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ ensure
+ ActionView::Helpers::FormHelper.form_with_generates_ids = old_value
+ end
+
def test_form_for_with_collection_radio_buttons
post = Post.new
def post.active; false; end
@@ -1960,6 +1995,34 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_form_for_default_enforce_utf8_false
+ with_default_enforce_utf8 false do
+ form_for(:post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", nil, nil, enforce_utf8: false) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_form_for_default_enforce_utf8_true
+ with_default_enforce_utf8 true do
+ form_for(:post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", nil, nil, enforce_utf8: true) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
def test_form_for_with_remote_in_html
form_for(@post, url: "/", html: { remote: true, id: "create-post", method: :patch }) do |f|
concat f.text_field(:title)
@@ -2239,7 +2302,7 @@ class FormHelperTest < ActionView::TestCase
end
end
- def test_submit_with_object_and_nested_lookup
+ def test_submit_with_object_which_is_overwritten_by_as_option
with_locale :submit do
form_for(@post, as: :another_post) do |f|
concat f.submit
@@ -2253,6 +2316,21 @@ class FormHelperTest < ActionView::TestCase
end
end
+ def test_submit_with_object_which_is_namespaced
+ blog_post = Blog::Post.new("And his name will be forty and four.", 44)
+ with_locale :submit do
+ form_for(blog_post) do |f|
+ concat f.submit
+ end
+
+ expected = whole_form("/posts/44", "edit_post_44", "edit_post", method: "patch") do
+ "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
def test_nested_fields_for
@comment.body = "Hello World"
form_for(@post) do |f|
@@ -3519,4 +3597,13 @@ class FormHelperTest < ActionView::TestCase
ensure
I18n.locale = old_locale
end
+
+ def with_default_enforce_utf8(value)
+ old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value
+
+ yield
+ ensure
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value
+ end
end
diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb
index a66db2f3dc..8f796bdb83 100644
--- a/actionview/test/template/form_options_helper_test.rb
+++ b/actionview/test/template/form_options_helper_test.rb
@@ -337,8 +337,24 @@ class FormOptionsHelperTest < ActionView::TestCase
)
end
+ def test_option_groups_from_collection_for_select_with_callable_group_method
+ group_proc = Proc.new { |c| c.countries }
+ assert_dom_equal(
+ "<optgroup label=\"&lt;Africa&gt;\"><option value=\"&lt;sa&gt;\">&lt;South Africa&gt;</option>\n<option value=\"so\">Somalia</option></optgroup><optgroup label=\"Europe\"><option value=\"dk\" selected=\"selected\">Denmark</option>\n<option value=\"ie\">Ireland</option></optgroup>",
+ option_groups_from_collection_for_select(dummy_continents, group_proc, "continent_name", "country_id", "country_name", "dk")
+ )
+ end
+
+ def test_option_groups_from_collection_for_select_with_callable_group_label_method
+ label_proc = Proc.new { |c| c.continent_name }
+ assert_dom_equal(
+ "<optgroup label=\"&lt;Africa&gt;\"><option value=\"&lt;sa&gt;\">&lt;South Africa&gt;</option>\n<option value=\"so\">Somalia</option></optgroup><optgroup label=\"Europe\"><option value=\"dk\" selected=\"selected\">Denmark</option>\n<option value=\"ie\">Ireland</option></optgroup>",
+ option_groups_from_collection_for_select(dummy_continents, "countries", label_proc, "country_id", "country_name", "dk")
+ )
+ end
+
def test_option_groups_from_collection_for_select_returns_html_safe_string
- assert option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk").html_safe?
+ assert_predicate option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk"), :html_safe?
end
def test_grouped_options_for_select_with_array
@@ -367,7 +383,7 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
"<optgroup label=\"----------\"><option value=\"US\">US</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"----------\"><option value=\"GB\">GB</option>\n<option value=\"Germany\">Germany</option></optgroup>",
- grouped_options_for_select([["US", "Canada"] , ["GB", "Germany"]], nil, divider: "----------")
+ grouped_options_for_select([["US", "Canada"], ["GB", "Germany"]], nil, divider: "----------")
)
end
@@ -386,7 +402,7 @@ class FormOptionsHelperTest < ActionView::TestCase
end
def test_grouped_options_for_select_returns_html_safe_string
- assert grouped_options_for_select([["Hats", ["Baseball Cap", "Cowboy Hat"]]]).html_safe?
+ assert_predicate grouped_options_for_select([["Hats", ["Baseball Cap", "Cowboy Hat"]]]), :html_safe?
end
def test_grouped_options_for_select_with_prompt_returns_html_escaped_string
@@ -476,7 +492,7 @@ class FormOptionsHelperTest < ActionView::TestCase
end
def test_time_zone_options_returns_html_safe_string
- assert time_zone_options_for_select.html_safe?
+ assert_predicate time_zone_options_for_select, :html_safe?
end
def test_select
@@ -495,6 +511,16 @@ class FormOptionsHelperTest < ActionView::TestCase
)
end
+ def test_required_select_with_default_and_selected_placeholder
+ assert_dom_equal(
+ ['<select required="required" name="post[category]" id="post_category"><option disabled="disabled" selected="selected" value="">Choose one</option>',
+ '<option value="lifestyle">lifestyle</option>',
+ '<option value="programming">programming</option>',
+ '<option value="spiritual">spiritual</option></select>'].join("\n"),
+ select(:post, :category, ["lifestyle", "programming", "spiritual"], { selected: "", disabled: "", prompt: "Choose one" }, { required: true })
+ )
+ end
+
def test_select_with_grouped_collection_as_nested_array
@post = Post.new
@@ -1251,6 +1277,25 @@ class FormOptionsHelperTest < ActionView::TestCase
html
end
+ def test_time_zone_select_with_priority_zones_and_errors
+ @firm = Firm.new("D")
+ @firm.extend ActiveModel::Validations
+ @firm.errors[:time_zone] << "invalid"
+ zones = [ ActiveSupport::TimeZone.new("A"), ActiveSupport::TimeZone.new("D") ]
+ html = time_zone_select("firm", "time_zone", zones)
+ assert_dom_equal "<div class=\"field_with_errors\">" \
+ "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>" \
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"E\">E</option>" \
+ "</select>" \
+ "</div>",
+ html
+ end
+
def test_time_zone_select_with_default_time_zone_and_nil_value
@firm = Firm.new()
@firm.time_zone = nil
diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb
index 5e328ebf53..a3500a7eb3 100644
--- a/actionview/test/template/form_tag_helper_test.rb
+++ b/actionview/test/template/form_tag_helper_test.rb
@@ -142,14 +142,32 @@ class FormTagHelperTest < ActionView::TestCase
actual = form_tag({}, { enforce_utf8: true })
expected = whole_form("http://www.example.com", enforce_utf8: true)
assert_dom_equal expected, actual
- assert actual.html_safe?
+ assert_predicate actual, :html_safe?
end
def test_form_tag_enforce_utf8_false
actual = form_tag({}, { enforce_utf8: false })
expected = whole_form("http://www.example.com", enforce_utf8: false)
assert_dom_equal expected, actual
- assert actual.html_safe?
+ assert_predicate actual, :html_safe?
+ end
+
+ def test_form_tag_default_enforce_utf8_false
+ with_default_enforce_utf8 false do
+ actual = form_tag({})
+ expected = whole_form("http://www.example.com", enforce_utf8: false)
+ assert_dom_equal expected, actual
+ assert_predicate actual, :html_safe?
+ end
+ end
+
+ def test_form_tag_default_enforce_utf8_true
+ with_default_enforce_utf8 true do
+ actual = form_tag({})
+ expected = whole_form("http://www.example.com", enforce_utf8: true)
+ assert_dom_equal expected, actual
+ assert_predicate actual, :html_safe?
+ end
end
def test_form_tag_with_block_in_erb
@@ -782,4 +800,13 @@ class FormTagHelperTest < ActionView::TestCase
def root_elem(rendered_content)
Nokogiri::HTML::DocumentFragment.parse(rendered_content).children.first # extract from nodeset
end
+
+ def with_default_enforce_utf8(value)
+ old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value
+
+ yield
+ ensure
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value
+ end
end
diff --git a/actionview/test/template/javascript_helper_test.rb b/actionview/test/template/javascript_helper_test.rb
index 4478c9f4ab..a72bc6c2fe 100644
--- a/actionview/test/template/javascript_helper_test.rb
+++ b/actionview/test/template/javascript_helper_test.rb
@@ -6,11 +6,15 @@ class JavaScriptHelperTest < ActionView::TestCase
tests ActionView::Helpers::JavaScriptHelper
attr_accessor :output_buffer
+ attr_reader :request
setup do
@old_escape_html_entities_in_json = ActiveSupport.escape_html_entities_in_json
ActiveSupport.escape_html_entities_in_json = true
@template = self
+ @request = Class.new do
+ def send_early_hints(links) end
+ end.new
end
def teardown
diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb
index a4d89ba0d1..7f4fd25573 100644
--- a/actionview/test/template/log_subscriber_test.rb
+++ b/actionview/test/template/log_subscriber_test.rb
@@ -30,7 +30,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
ActiveSupport::LogSubscriber.log_subscribers.clear
# We need to undef `root`, RenderTestCases don't want this to be defined
- Rails.instance_eval { undef :root } if @defined_root
+ Rails.instance_eval { undef :root } if defined?(@defined_root)
end
def set_logger(logger)
diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb
index 402ee9b6ae..38469cbe3d 100644
--- a/actionview/test/template/lookup_context_test.rb
+++ b/actionview/test/template/lookup_context_test.rb
@@ -35,7 +35,7 @@ class LookupContextTest < ActiveSupport::TestCase
test "allows me to freeze and retrieve frozen formats" do
@lookup_context.formats.freeze
- assert @lookup_context.formats.frozen?
+ assert_predicate @lookup_context.formats, :frozen?
end
test "provides getters and setters for variants" do
@@ -195,7 +195,7 @@ class LookupContextTest < ActiveSupport::TestCase
assert @lookup_context.cache
template = @lookup_context.disable_cache do
- assert !@lookup_context.cache
+ assert_not @lookup_context.cache
@lookup_context.find("foo", %w(test), true)
end
assert @lookup_context.cache
diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb
index 2b671a6685..357ae1326a 100644
--- a/actionview/test/template/number_helper_test.rb
+++ b/actionview/test/template/number_helper_test.rb
@@ -79,7 +79,7 @@ class NumberHelperTest < ActionView::TestCase
assert_equal "1.23 &lt;b&gt;km3&lt;/b&gt;", number_to_human(1_234_567_000_000, units: volume)
assert_equal "1.23 &lt;b&gt;Pl&lt;/b&gt;", number_to_human(1_234_567_000_000_000, units: volume)
- #Including fractionals
+ # Including fractionals
distance = { mili: "<b>mm</b>", centi: "<b>cm</b>", deci: "<b>dm</b>", unit: "<b>m</b>",
ten: "<b>dam</b>", hundred: "<b>hm</b>", thousand: "<b>km</b>",
micro: "<b>um</b>", nano: "<b>nm</b>", pico: "<b>pm</b>", femto: "<b>fm</b>" }
@@ -126,43 +126,43 @@ class NumberHelperTest < ActionView::TestCase
end
def test_number_helpers_outputs_are_html_safe
- assert number_to_human(1).html_safe?
- assert !number_to_human("<script></script>").html_safe?
- assert number_to_human("asdf".html_safe).html_safe?
- assert number_to_human("1".html_safe).html_safe?
-
- assert number_to_human_size(1).html_safe?
- assert number_to_human_size(1000000).html_safe?
- assert !number_to_human_size("<script></script>").html_safe?
- assert number_to_human_size("asdf".html_safe).html_safe?
- assert number_to_human_size("1".html_safe).html_safe?
-
- assert number_with_precision(1, strip_insignificant_zeros: false).html_safe?
- assert number_with_precision(1, strip_insignificant_zeros: true).html_safe?
- assert !number_with_precision("<script></script>").html_safe?
- assert number_with_precision("asdf".html_safe).html_safe?
- assert number_with_precision("1".html_safe).html_safe?
-
- assert number_to_currency(1).html_safe?
- assert !number_to_currency("<script></script>").html_safe?
- assert number_to_currency("asdf".html_safe).html_safe?
- assert number_to_currency("1".html_safe).html_safe?
-
- assert number_to_percentage(1).html_safe?
- assert !number_to_percentage("<script></script>").html_safe?
- assert number_to_percentage("asdf".html_safe).html_safe?
- assert number_to_percentage("1".html_safe).html_safe?
-
- assert number_to_phone(1).html_safe?
+ assert_predicate number_to_human(1), :html_safe?
+ assert_not_predicate number_to_human("<script></script>"), :html_safe?
+ assert_predicate number_to_human("asdf".html_safe), :html_safe?
+ assert_predicate number_to_human("1".html_safe), :html_safe?
+
+ assert_predicate number_to_human_size(1), :html_safe?
+ assert_predicate number_to_human_size(1000000), :html_safe?
+ assert_not_predicate number_to_human_size("<script></script>"), :html_safe?
+ assert_predicate number_to_human_size("asdf".html_safe), :html_safe?
+ assert_predicate number_to_human_size("1".html_safe), :html_safe?
+
+ assert_predicate number_with_precision(1, strip_insignificant_zeros: false), :html_safe?
+ assert_predicate number_with_precision(1, strip_insignificant_zeros: true), :html_safe?
+ assert_not_predicate number_with_precision("<script></script>"), :html_safe?
+ assert_predicate number_with_precision("asdf".html_safe), :html_safe?
+ assert_predicate number_with_precision("1".html_safe), :html_safe?
+
+ assert_predicate number_to_currency(1), :html_safe?
+ assert_not_predicate number_to_currency("<script></script>"), :html_safe?
+ assert_predicate number_to_currency("asdf".html_safe), :html_safe?
+ assert_predicate number_to_currency("1".html_safe), :html_safe?
+
+ assert_predicate number_to_percentage(1), :html_safe?
+ assert_not_predicate number_to_percentage("<script></script>"), :html_safe?
+ assert_predicate number_to_percentage("asdf".html_safe), :html_safe?
+ assert_predicate number_to_percentage("1".html_safe), :html_safe?
+
+ assert_predicate number_to_phone(1), :html_safe?
assert_equal "&lt;script&gt;&lt;/script&gt;", number_to_phone("<script></script>")
- assert number_to_phone("<script></script>").html_safe?
- assert number_to_phone("asdf".html_safe).html_safe?
- assert number_to_phone("1".html_safe).html_safe?
-
- assert number_with_delimiter(1).html_safe?
- assert !number_with_delimiter("<script></script>").html_safe?
- assert number_with_delimiter("asdf".html_safe).html_safe?
- assert number_with_delimiter("1".html_safe).html_safe?
+ assert_predicate number_to_phone("<script></script>"), :html_safe?
+ assert_predicate number_to_phone("asdf".html_safe), :html_safe?
+ assert_predicate number_to_phone("1".html_safe), :html_safe?
+
+ assert_predicate number_with_delimiter(1), :html_safe?
+ assert_not_predicate number_with_delimiter("<script></script>"), :html_safe?
+ assert_predicate number_with_delimiter("asdf".html_safe), :html_safe?
+ assert_predicate number_with_delimiter("1".html_safe), :html_safe?
end
def test_number_helpers_should_raise_error_if_invalid_when_specified
diff --git a/actionview/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb
index b5e9a77105..faeeded1c8 100644
--- a/actionview/test/template/output_safety_helper_test.rb
+++ b/actionview/test/template/output_safety_helper_test.rb
@@ -12,7 +12,7 @@ class OutputSafetyHelperTest < ActionView::TestCase
test "raw returns the safe string" do
result = raw(@string)
assert_equal @string, result
- assert result.html_safe?
+ assert_predicate result, :html_safe?
end
test "raw handles nil values correctly" do
@@ -53,11 +53,11 @@ class OutputSafetyHelperTest < ActionView::TestCase
test "to_sentence should escape non-html_safe values" do
actual = to_sentence(%w(< > & ' "))
- assert actual.html_safe?
+ assert_predicate actual, :html_safe?
assert_equal("&lt;, &gt;, &amp;, &#39;, and &quot;", actual)
actual = to_sentence(%w(<script>))
- assert actual.html_safe?
+ assert_predicate actual, :html_safe?
assert_equal("&lt;script&gt;", actual)
end
@@ -80,19 +80,19 @@ class OutputSafetyHelperTest < ActionView::TestCase
url = "https://example.com"
expected = %(<a href="#{url}">#{url}</a> and <p>&lt;marquee&gt;shady stuff&lt;/marquee&gt;<br /></p>)
actual = to_sentence([link_to(url, url), ptag])
- assert actual.html_safe?
+ assert_predicate actual, :html_safe?
assert_equal(expected, actual)
end
test "to_sentence handles blank strings" do
actual = to_sentence(["", "two", "three"])
- assert actual.html_safe?
+ assert_predicate actual, :html_safe?
assert_equal ", two, and three", actual
end
test "to_sentence handles nil values" do
actual = to_sentence([nil, "two", "three"])
- assert actual.html_safe?
+ assert_predicate actual, :html_safe?
assert_equal ", two, and three", actual
end
diff --git a/actionview/test/template/record_tag_helper_test.rb b/actionview/test/template/record_tag_helper_test.rb
deleted file mode 100644
index 7bbbfccdd0..0000000000
--- a/actionview/test/template/record_tag_helper_test.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require "abstract_unit"
-
-class RecordTagPost
- extend ActiveModel::Naming
-
- attr_accessor :id, :body
-
- def initialize
- @id = 45
- @body = "What a wonderful world!"
-
- yield self if block_given?
- end
-end
-
-class RecordTagHelperTest < ActionView::TestCase
- tests ActionView::Helpers::RecordTagHelper
-
- def setup
- super
- @post = RecordTagPost.new
- end
-
- def test_content_tag_for
- assert_raises(NoMethodError) { content_tag_for(:li, @post) }
- end
-
- def test_div_for
- assert_raises(NoMethodError) { div_for(@post, class: "special") }
- end
-end
diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb
index c7714cf205..181f09ab65 100644
--- a/actionview/test/template/sanitize_helper_test.rb
+++ b/actionview/test/template/sanitize_helper_test.rb
@@ -21,8 +21,8 @@ class SanitizeHelperTest < ActionView::TestCase
def test_should_sanitize_illegal_style_properties
raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;)
- expected = %(display: block; width: 100%; height: 100%; background-color: black; background-x: center; background-y: center;)
- assert_equal expected, sanitize_css(raw)
+ expected = %r(\Adisplay:\s?block;\s?width:\s?100%;\s?height:\s?100%;\s?background-color:\s?black;\s?background-x:\s?center;\s?background-y:\s?center;\z)
+ assert_match expected, sanitize_css(raw)
end
def test_strip_tags
@@ -38,6 +38,6 @@ class SanitizeHelperTest < ActionView::TestCase
end
def test_sanitize_is_marked_safe
- assert sanitize("<html><script></script></html>").html_safe?
+ assert_predicate sanitize("<html><script></script></html>"), :html_safe?
end
end
diff --git a/actionview/test/template/streaming_render_test.rb b/actionview/test/template/streaming_render_test.rb
index 23edf7b538..ef000300cc 100644
--- a/actionview/test/template/streaming_render_test.rb
+++ b/actionview/test/template/streaming_render_test.rb
@@ -5,7 +5,7 @@ require "abstract_unit"
class TestController < ActionController::Base
end
-class FiberedTest < ActiveSupport::TestCase
+class SetupFiberedBase < ActiveSupport::TestCase
def setup
view_paths = ActionController::Base.view_paths
@assigns = { secret: "in the sauce", name: nil }
@@ -25,7 +25,9 @@ class FiberedTest < ActiveSupport::TestCase
end
string
end
+end
+class FiberedTest < SetupFiberedBase
def test_streaming_works
content = []
body = render_body(template: "test/hello_world", layout: "layouts/yield")
@@ -111,3 +113,20 @@ class FiberedTest < ActiveSupport::TestCase
buffered_render(template: "test/streaming", layout: "layouts/streaming_with_capture")
end
end
+
+class FiberedWithLocaleTest < SetupFiberedBase
+ def setup
+ @old_locale = I18n.locale
+ I18n.locale = "da"
+ super
+ end
+
+ def teardown
+ I18n.locale = @old_locale
+ end
+
+ def test_render_with_streaming_and_locale
+ assert_equal "layout.locale: da\nview.locale: da\n\n",
+ buffered_render(template: "test/streaming_with_locale", layout: "layouts/streaming_with_locale")
+ end
+end
diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb
index 8c57803796..9a6226fd04 100644
--- a/actionview/test/template/tag_helper_test.rb
+++ b/actionview/test/template/tag_helper_test.rb
@@ -81,7 +81,7 @@ class TagHelperTest < ActionView::TestCase
def test_content_tag
assert_equal "<a href=\"create\">Create</a>", content_tag("a", "Create", "href" => "create")
- assert content_tag("a", "Create", "href" => "create").html_safe?
+ assert_predicate content_tag("a", "Create", "href" => "create"), :html_safe?
assert_equal content_tag("a", "Create", "href" => "create"),
content_tag("a", "Create", href: "create")
assert_equal "<p>&lt;script&gt;evil_js&lt;/script&gt;</p>",
@@ -92,7 +92,7 @@ class TagHelperTest < ActionView::TestCase
def test_tag_builder_with_content
assert_equal "<div id=\"post_1\">Content</div>", tag.div("Content", id: "post_1")
- assert tag.div("Content", id: "post_1").html_safe?
+ assert_predicate tag.div("Content", id: "post_1"), :html_safe?
assert_equal tag.div("Content", id: "post_1"),
tag.div("Content", "id": "post_1")
assert_equal "<p>&lt;script&gt;evil_js&lt;/script&gt;</p>",
@@ -307,8 +307,8 @@ class TagHelperTest < ActionView::TestCase
def test_tag_builder_disable_escaping
assert_equal '<a href="&amp;"></a>', tag.a(href: "&amp;", escape_attributes: false)
- assert_equal '<a href="&amp;">cnt</a>', tag.a(href: "&amp;" , escape_attributes: false) { "cnt" }
- assert_equal '<br data-hidden="&amp;">', tag.br("data-hidden": "&amp;" , escape_attributes: false)
+ assert_equal '<a href="&amp;">cnt</a>', tag.a(href: "&amp;", escape_attributes: false) { "cnt" }
+ assert_equal '<br data-hidden="&amp;">', tag.br("data-hidden": "&amp;", escape_attributes: false)
assert_equal '<a href="&amp;">content</a>', tag.a("content", href: "&amp;", escape_attributes: false)
assert_equal '<a href="&amp;">content</a>', tag.a(href: "&amp;", escape_attributes: false) { "content" }
end
@@ -316,18 +316,18 @@ class TagHelperTest < ActionView::TestCase
def test_data_attributes
["data", :data].each { |data|
assert_dom_equal '<a data-a-float="3.14" data-a-big-decimal="-123.456" data-a-number="1" data-array="[1,2,3]" data-hash="{&quot;key&quot;:&quot;value&quot;}" data-string-with-quotes="double&quot;quote&quot;party&quot;" data-string="hello" data-symbol="foo" />',
- tag("a", data => { 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"' })
+ tag("a", data => { a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' })
assert_dom_equal '<a data-a-float="3.14" data-a-big-decimal="-123.456" data-a-number="1" data-array="[1,2,3]" data-hash="{&quot;key&quot;:&quot;value&quot;}" data-string-with-quotes="double&quot;quote&quot;party&quot;" data-string="hello" data-symbol="foo" />',
- tag.a(data: { 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"' })
+ tag.a(data: { a_float: 3.14, a_big_decimal: BigDecimal("-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_aria_attributes
["aria", :aria].each { |aria|
assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-array="[1,2,3]" aria-hash="{&quot;key&quot;:&quot;value&quot;}" aria-string-with-quotes="double&quot;quote&quot;party&quot;" aria-string="hello" aria-symbol="foo" />',
- 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"' })
+ tag("a", aria => { a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' })
assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-array="[1,2,3]" aria-hash="{&quot;key&quot;:&quot;value&quot;}" aria-string-with-quotes="double&quot;quote&quot;party&quot;" aria-string="hello" aria-symbol="foo" />',
- 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"' })
+ tag.a(aria: { a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' })
}
end
diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb
index 05e5f21ce4..d98fd4f9a2 100644
--- a/actionview/test/template/test_case_test.rb
+++ b/actionview/test/template/test_case_test.rb
@@ -192,7 +192,7 @@ module ActionView
helper HelperThatInvokesProtectAgainstForgery
test "protect_from_forgery? in any helpers returns false" do
- assert !view.help_me
+ assert_not view.help_me
end
end
diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb
index f247de066f..45edfe18be 100644
--- a/actionview/test/template/text_helper_test.rb
+++ b/actionview/test/template/text_helper_test.rb
@@ -19,12 +19,12 @@ class TextHelperTest < ActionView::TestCase
end
def test_simple_format_should_be_html_safe
- assert simple_format("<b> test with html tags </b>").html_safe?
+ assert_predicate simple_format("<b> test with html tags </b>"), :html_safe?
end
def test_simple_format_included_in_isolation
helper_klass = Class.new { include ActionView::Helpers::TextHelper }
- assert helper_klass.new.simple_format("<b> test with html tags </b>").html_safe?
+ assert_predicate helper_klass.new.simple_format("<b> test with html tags </b>"), :html_safe?
end
def test_simple_format
@@ -123,7 +123,7 @@ class TextHelperTest < ActionView::TestCase
end
def test_truncate_should_be_html_safe
- assert truncate("Hello World!", length: 12).html_safe?
+ assert_predicate truncate("Hello World!", length: 12), :html_safe?
end
def test_truncate_should_escape_the_input
@@ -136,12 +136,12 @@ class TextHelperTest < ActionView::TestCase
def test_truncate_with_escape_false_should_be_html_safe
truncated = truncate("Hello <script>code!</script>World!!", length: 12, escape: false)
- assert truncated.html_safe?
+ assert_predicate truncated, :html_safe?
end
def test_truncate_with_block_should_be_html_safe
truncated = truncate("Here's a long test and I need a continue to read link", length: 27) { link_to "Continue", "#" }
- assert truncated.html_safe?
+ assert_predicate truncated, :html_safe?
end
def test_truncate_with_block_should_escape_the_input
@@ -156,7 +156,7 @@ class TextHelperTest < ActionView::TestCase
def test_truncate_with_block_with_escape_false_should_be_html_safe
truncated = truncate("<script>code!</script>Here's a long test and I need a continue to read link", length: 27, escape: false) { link_to "Continue", "#" }
- assert truncated.html_safe?
+ assert_predicate truncated, :html_safe?
end
def test_truncate_with_block_should_escape_the_block
@@ -165,7 +165,7 @@ class TextHelperTest < ActionView::TestCase
end
def test_highlight_should_be_html_safe
- assert highlight("This is a beautiful morning", "beautiful").html_safe?
+ assert_predicate highlight("This is a beautiful morning", "beautiful"), :html_safe?
end
def test_highlight
@@ -297,7 +297,7 @@ class TextHelperTest < ActionView::TestCase
end
def test_excerpt_should_not_be_html_safe
- assert !excerpt("This is a beautiful! morning", "beautiful", radius: 5).html_safe?
+ assert_not_predicate excerpt("This is a beautiful! morning", "beautiful", radius: 5), :html_safe?
end
def test_excerpt_in_borderline_cases
diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb
index 8956a584ff..f40595bf4d 100644
--- a/actionview/test/template/translation_helper_test.rb
+++ b/actionview/test/template/translation_helper_test.rb
@@ -75,7 +75,7 @@ class TranslationHelperTest < ActiveSupport::TestCase
def test_returns_missing_translation_message_with_unescaped_interpolation
expected = '<span class="translation_missing" title="translation missing: en.translations.missing, name: Kir, year: 2015, vulnerable: &amp;quot; onclick=&amp;quot;alert()&amp;quot;">Missing</span>'
assert_equal expected, translate(:"translations.missing", name: "Kir", year: "2015", vulnerable: %{" onclick="alert()"})
- assert translate(:"translations.missing").html_safe?
+ assert_predicate translate(:"translations.missing"), :html_safe?
end
def test_returns_missing_translation_message_does_filters_out_i18n_options
@@ -145,11 +145,11 @@ class TranslationHelperTest < ActiveSupport::TestCase
end
def test_translate_marks_translations_named_html_as_safe_html
- assert translate(:'translations.html').html_safe?
+ assert_predicate translate(:'translations.html'), :html_safe?
end
def test_translate_marks_translations_with_a_html_suffix_as_safe_html
- assert translate(:'translations.hello_html').html_safe?
+ assert_predicate translate(:'translations.hello_html'), :html_safe?
end
def test_translate_escapes_interpolations_in_translations_with_a_html_suffix
diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb
index 0cd0386cac..08cb5dfea7 100644
--- a/actionview/test/template/url_helper_test.rb
+++ b/actionview/test/template/url_helper_test.rb
@@ -508,16 +508,16 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_current_page_considering_params
@request = request_for_url("/?order=desc&page=1")
- assert !current_page?(url_hash, check_parameters: true)
- assert !current_page?(url_hash.merge(check_parameters: true))
- assert !current_page?(ActionController::Parameters.new(url_hash.merge(check_parameters: true)).permit!)
- assert !current_page?("http://www.example.com/", check_parameters: true)
+ assert_not current_page?(url_hash, check_parameters: true)
+ assert_not current_page?(url_hash.merge(check_parameters: true))
+ assert_not current_page?(ActionController::Parameters.new(url_hash.merge(check_parameters: true)).permit!)
+ assert_not current_page?("http://www.example.com/", check_parameters: true)
end
def test_current_page_considering_params_when_options_does_not_respond_to_to_hash
@request = request_for_url("/?order=desc&page=1")
- assert !current_page?(:back, check_parameters: false)
+ assert_not current_page?(:back, check_parameters: false)
end
def test_current_page_with_params_that_match
@@ -562,7 +562,7 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_current_page_with_not_get_verb
@request = request_for_url("/events", method: :post)
- assert !current_page?("/events")
+ assert_not current_page?("/events")
end
def test_link_unless_current
@@ -663,7 +663,7 @@ class UrlHelperTest < ActiveSupport::TestCase
end
def test_mail_to_returns_html_safe_string
- assert mail_to("david@loudthinking.com").html_safe?
+ assert_predicate mail_to("david@loudthinking.com"), :html_safe?
end
def test_mail_to_with_block
diff --git a/actionview/test/tmp/.keep b/actionview/test/tmp/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/actionview/test/tmp/.keep
+++ /dev/null
diff --git a/actionview/test/ujs/.gitignore b/actionview/test/ujs/.gitignore
deleted file mode 100644
index 31dbbff57c..0000000000
--- a/actionview/test/ujs/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/log
diff --git a/actionview/test/ujs/public/test/call-ajax.js b/actionview/test/ujs/public/test/call-ajax.js
new file mode 100644
index 0000000000..4d0bfb0806
--- /dev/null
+++ b/actionview/test/ujs/public/test/call-ajax.js
@@ -0,0 +1,26 @@
+(function() {
+
+module('call-ajax', {
+ setup: function() {
+ $('#qunit-fixture')
+ .append($('<a />', { href: '#' }))
+ }
+})
+
+asyncTest('call ajax without "ajax:beforeSend"', 1, function() {
+ var link = $('#qunit-fixture a')
+ link.bindNative('click', function() {
+ Rails.ajax({
+ type: 'get',
+ url: '/',
+ success: function() {
+ ok(true, 'calling request in ajax:success')
+ }
+ })
+ })
+
+ link.triggerNative('click')
+ setTimeout(function() { start() }, 50)
+})
+
+})()
diff --git a/actionview/test/ujs/public/test/call-remote-callbacks.js b/actionview/test/ujs/public/test/call-remote-callbacks.js
index 707e21541d..9c0c8cfb4b 100644
--- a/actionview/test/ujs/public/test/call-remote-callbacks.js
+++ b/actionview/test/ujs/public/test/call-remote-callbacks.js
@@ -12,34 +12,20 @@ module('call-remote-callbacks', {
$(document).undelegate('form[data-remote]', 'ajax:send')
$(document).undelegate('form[data-remote]', 'ajax:complete')
$(document).undelegate('form[data-remote]', 'ajax:success')
- $(document).unbind('ajaxStop')
$(document).unbind('iframe:loading')
}
})
-function start_after_submit(form) {
- form.bindNative('ajax:complete', function() {
- ok(true, 'ajax:complete')
- start()
- })
-}
-
function submit(fn) {
var form = $('form')
- start_after_submit(form)
if (fn) fn(form)
form.triggerNative('submit')
-}
-function submit_with_button(submit_button) {
- var form = $('form')
- start_after_submit(form)
-
- submit_button.triggerNative('click')
+ setTimeout(function() { start() }, 13)
}
-asyncTest('modifying form fields with "ajax:before" sends modified data in request', 4, function() {
+asyncTest('modifying form fields with "ajax:before" sends modified data in request', 3, function() {
$('form[data-remote]')
.append($('<input type="text" name="user_name" value="john">'))
.append($('<input type="text" name="removed_user_name" value="john">'))
@@ -61,7 +47,7 @@ asyncTest('modifying form fields with "ajax:before" sends modified data in reque
})
})
-asyncTest('modifying data("type") with "ajax:before" requests new dataType in request', 2, function() {
+asyncTest('modifying data("type") with "ajax:before" requests new dataType in request', 1, function() {
$('form[data-remote]').data('type', 'html')
.bindNative('ajax:before', function() {
this.setAttribute('data-type', 'xml')
@@ -74,7 +60,7 @@ asyncTest('modifying data("type") with "ajax:before" requests new dataType in re
})
})
-asyncTest('setting data("with-credentials",true) with "ajax:before" uses new setting in request', 2, function() {
+asyncTest('setting data("with-credentials",true) with "ajax:before" uses new setting in request', 1, function() {
$('form[data-remote]').data('with-credentials', false)
.bindNative('ajax:before', function() {
this.setAttribute('data-with-credentials', true)
@@ -89,21 +75,18 @@ asyncTest('setting data("with-credentials",true) with "ajax:before" uses new set
asyncTest('stopping the "ajax:beforeSend" event aborts the request', 1, function() {
submit(function(form) {
- form.bindNative('ajax:beforeSend', function() {
+ form.bindNative('ajax:beforeSend', function(e) {
ok(true, 'aborting request in ajax:beforeSend')
- return false
+ e.preventDefault()
})
form.unbind('ajax:send').bindNative('ajax:send', function() {
ok(false, 'ajax:send should not run')
})
- form.unbind('ajax:complete').bindNative('ajax:complete', function() {
- ok(false, 'ajax:complete should not run')
- })
- form.bindNative('ajax:error', function(e, xhr, status, error) {
+ form.bindNative('ajax:error', function(e, response, status, xhr) {
ok(false, 'ajax:error should not run')
})
- $(document).bindNative('ajaxStop', function() {
- start()
+ form.bindNative('ajax:complete', function() {
+ ok(false, 'ajax:complete should not run')
})
})
})
@@ -165,8 +148,8 @@ function skipIt() {
.bind('iframe:loading', function() {
ok(false, 'form should not get submitted')
})
- .bindNative('ajax:aborted:file', function() {
- return false
+ .bindNative('ajax:aborted:file', function(e) {
+ e.preventDefault()
})
.triggerNative('submit')
@@ -179,25 +162,22 @@ function skipIt() {
}
asyncTest('"ajax:beforeSend" can be observed and stopped with event delegation', 1, function() {
- $(document).delegate('form[data-remote]', 'ajax:beforeSend', function() {
+ $(document).delegate('form[data-remote]', 'ajax:beforeSend', function(e) {
ok(true, 'ajax:beforeSend observed with event delegation')
- return false
+ e.preventDefault()
})
submit(function(form) {
form.unbind('ajax:send').bindNative('ajax:send', function() {
ok(false, 'ajax:send should not run')
})
- form.unbind('ajax:complete').bindNative('ajax:complete', function() {
+ form.bindNative('ajax:complete', function() {
ok(false, 'ajax:complete should not run')
})
- $(document).bindNative('ajaxStop', function() {
- start()
- })
})
})
-asyncTest('"ajax:beforeSend", "ajax:send", "ajax:success" and "ajax:complete" are triggered', 9, function() {
+asyncTest('"ajax:beforeSend", "ajax:send", "ajax:success" and "ajax:complete" are triggered', 8, function() {
submit(function(form) {
form.bindNative('ajax:beforeSend', function(e, xhr, settings) {
ok(xhr.setRequestHeader, 'first argument to "ajax:beforeSend" should be an XHR object')
@@ -218,25 +198,25 @@ asyncTest('"ajax:beforeSend", "ajax:send", "ajax:success" and "ajax:complete" ar
})
})
-if(window.phantom !== undefined) {
- asyncTest('"ajax:beforeSend", "ajax:send", "ajax:error" and "ajax:complete" are triggered on error', 7, function() {
- submit(function(form) {
- form.attr('action', '/error')
- form.bindNative('ajax:beforeSend', function(arg) { ok(true, 'ajax:beforeSend') })
- form.bindNative('ajax:send', function(arg) { ok(true, 'ajax:send') })
- form.bindNative('ajax:error', function(e, xhr, status, error) {
- ok(xhr.getResponseHeader, 'first argument to "ajax:error" should be an XHR object')
- equal(status, 'error', 'second argument to ajax:error should be a status string')
- // Firefox 8 returns "Forbidden " with trailing space
- equal($.trim(error), 'Forbidden', 'third argument to ajax:error should be an HTTP status response')
- // Opera returns "0" for HTTP code
- equal(xhr.status, window.opera ? 0 : 403, 'status code should be 403')
- })
+asyncTest('"ajax:beforeSend", "ajax:send", "ajax:error" and "ajax:complete" are triggered on error', 8, function() {
+ submit(function(form) {
+ form.attr('action', '/error')
+ form.bindNative('ajax:beforeSend', function(arg) { ok(true, 'ajax:beforeSend') })
+ form.bindNative('ajax:send', function(arg) { ok(true, 'ajax:send') })
+ form.bindNative('ajax:error', function(e, response, status, xhr) {
+ equal(response, '', 'first argument to ajax:error should be an HTTP status response')
+ equal(status, 'Forbidden', 'second argument to ajax:error should be a status string')
+ ok(xhr.getResponseHeader, 'third argument to "ajax:error" should be an XHR object')
+ // Opera returns "0" for HTTP code
+ equal(xhr.status, window.opera ? 0 : 403, 'status code should be 403')
+ })
+ form.bindNative('ajax:complete', function(e, xhr, status) {
+ ok(xhr.getResponseHeader, 'first argument to "ajax:complete" should be an XHR object')
+ equal(status, 'Forbidden', 'second argument to ajax:complete should be a status string')
})
})
-}
+})
-// IF THIS TEST IS FAILING, TRY INCREASING THE TIMEOUT AT THE BOTTOM TO > 100
asyncTest('binding to ajax callbacks via .delegate() triggers handlers properly', 4, function() {
$(document)
.delegate('form[data-remote]', 'ajax:beforeSend', function() {
@@ -245,29 +225,15 @@ asyncTest('binding to ajax callbacks via .delegate() triggers handlers properly'
.delegate('form[data-remote]', 'ajax:send', function() {
ok(true, 'ajax:send handler is triggered')
})
- .delegate('form[data-remote]', 'ajax:complete', function() {
- ok(true, 'ajax:complete handler is triggered')
- })
.delegate('form[data-remote]', 'ajax:success', function() {
ok(true, 'ajax:success handler is triggered')
})
- $('form[data-remote]').triggerNative('submit')
-
- setTimeout(function() {
- start()
- }, 63)
-})
-
-asyncTest('binding to ajax:send event to call jquery methods on ajax object', 2, function() {
- $('form[data-remote]')
- .bindNative('ajax:send', function(e, xhr) {
- ok(true, 'event should fire')
- equal(typeof(xhr.abort), 'function', 'event should pass jqXHR object')
- xhr.abort()
+ .delegate('form[data-remote]', 'ajax:complete', function() {
+ ok(true, 'ajax:complete handler is triggered')
})
- .triggerNative('submit')
+ $('form[data-remote]').triggerNative('submit')
- setTimeout(function() { start() }, 35)
+ setTimeout(function() { start() }, 13)
})
})()
diff --git a/actionview/test/ujs/public/test/call-remote.js b/actionview/test/ujs/public/test/call-remote.js
index 5932195363..778dc1b09a 100644
--- a/actionview/test/ujs/public/test/call-remote.js
+++ b/actionview/test/ujs/public/test/call-remote.js
@@ -128,6 +128,17 @@ asyncTest('execution of JS code does not modify current DOM', 1, function() {
})
})
+asyncTest('HTML content should be plain-text', 1, function() {
+ buildForm({ method: 'post', 'data-type': 'html' })
+
+ $('form').append('<input type="text" name="content_type" value="text/html">')
+ $('form').append('<input type="text" name="content" value="<p>hello</p>">')
+
+ submit(function(e, data, status, xhr) {
+ ok(data === '<p>hello</p>', 'returned data should be a plain-text string')
+ })
+})
+
asyncTest('XML document should be parsed', 1, function() {
buildForm({ method: 'post', 'data-type': 'html' })
@@ -199,7 +210,7 @@ asyncTest('allow empty form "action"', 1, function() {
buildForm({ action: '' })
$('#qunit-fixture').find('form')
- .bindNative('ajax:beforeSend', function(e, xhr, settings) {
+ .bindNative('ajax:beforeSend', function(evt, xhr, settings) {
// Get current location (the same way jQuery does)
try {
currentLocation = location.href
@@ -218,7 +229,7 @@ asyncTest('allow empty form "action"', 1, function() {
// Prevent the request from actually getting sent to the current page and
// causing an error.
- return false
+ evt.preventDefault()
})
.triggerNative('submit')
@@ -246,7 +257,7 @@ asyncTest('intelligently guesses crossDomain behavior when target URL has a diff
equal(settings.crossDomain, true, 'crossDomain should be set to true')
// prevent request from actually getting sent off-domain
- return false
+ evt.preventDefault()
})
.triggerNative('submit')
@@ -265,7 +276,7 @@ asyncTest('intelligently guesses crossDomain behavior when target URL consists o
equal(settings.crossDomain, false, 'crossDomain should be set to false')
// prevent request from actually getting sent off-domain
- return false
+ evt.preventDefault()
})
.triggerNative('submit')
diff --git a/actionview/test/ujs/public/test/data-confirm.js b/actionview/test/ujs/public/test/data-confirm.js
index d1ea82ea7e..1bd57b69ad 100644
--- a/actionview/test/ujs/public/test/data-confirm.js
+++ b/actionview/test/ujs/public/test/data-confirm.js
@@ -173,9 +173,9 @@ asyncTest('binding to confirm event of a link and returning false', 1, function(
}
$('a[data-confirm]')
- .bindNative('confirm', function() {
+ .bindNative('confirm', function(e) {
App.assertCallbackInvoked('confirm')
- return false
+ e.preventDefault()
})
.bindNative('confirm:complete', function() {
App.assertCallbackNotInvoked('confirm:complete')
@@ -194,9 +194,9 @@ asyncTest('binding to confirm event of a button and returning false', 1, functio
}
$('button[data-confirm]')
- .bindNative('confirm', function() {
+ .bindNative('confirm', function(e) {
App.assertCallbackInvoked('confirm')
- return false
+ e.preventDefault()
})
.bindNative('confirm:complete', function() {
App.assertCallbackNotInvoked('confirm:complete')
@@ -216,9 +216,9 @@ asyncTest('binding to confirm:complete event of a link and returning false', 2,
}
$('a[data-confirm]')
- .bindNative('confirm:complete', function() {
+ .bindNative('confirm:complete', function(e) {
App.assertCallbackInvoked('confirm:complete')
- return false
+ e.preventDefault()
})
.bindNative('ajax:beforeSend', function() {
App.assertCallbackNotInvoked('ajax:beforeSend')
@@ -238,9 +238,9 @@ asyncTest('binding to confirm:complete event of a button and returning false', 2
}
$('button[data-confirm]')
- .bindNative('confirm:complete', function() {
+ .bindNative('confirm:complete', function(e) {
App.assertCallbackInvoked('confirm:complete')
- return false
+ e.preventDefault()
})
.bindNative('ajax:beforeSend', function() {
App.assertCallbackNotInvoked('ajax:beforeSend')
@@ -314,3 +314,29 @@ asyncTest('clicking on the children of a disabled button should not trigger a co
start()
}, 50)
})
+
+asyncTest('clicking on a link with data-confirm attribute with custom confirm handler. Confirm yes.', 7, function() {
+ var message, element
+ // redefine confirm function so we can make sure it's not called
+ window.confirm = function(msg) {
+ ok(false, 'confirm dialog should not be called')
+ }
+ // custom auto-confirm:
+ Rails.confirm = function(msg, elem) { message = msg; element = elem; return true }
+
+ $('a[data-confirm]')
+ .bindNative('confirm:complete', function(e, data) {
+ App.assertCallbackInvoked('confirm:complete')
+ ok(data == true, 'confirm:complete passes in confirm answer (true)')
+ })
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ App.assertCallbackInvoked('ajax:success')
+ App.assertRequestPath(data, '/echo')
+ App.assertGetRequest(data)
+
+ equal(message, 'Are you absolutely sure?')
+ equal(element, $('a[data-confirm]').get(0))
+ start()
+ })
+ .triggerNative('click')
+})
diff --git a/actionview/test/ujs/public/test/data-disable-with.js b/actionview/test/ujs/public/test/data-disable-with.js
index b29cbbc867..645ad494c3 100644
--- a/actionview/test/ujs/public/test/data-disable-with.js
+++ b/actionview/test/ujs/public/test/data-disable-with.js
@@ -132,7 +132,8 @@ test('form input[type=submit][data-disable-with] re-enables when `pageshow` even
})
asyncTest('form[data-remote] input[type=submit][data-disable-with] is replaced in ajax callback', 2, function() {
- var form = $('form:not([data-remote])').attr('data-remote', 'true'), origFormContents = form.html()
+ var form = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'),
+ origFormContents = form.html()
form.bindNative('ajax:success', function() {
form.html(origFormContents)
@@ -146,7 +147,8 @@ asyncTest('form[data-remote] input[type=submit][data-disable-with] is replaced i
})
asyncTest('form[data-remote] input[data-disable-with] is replaced with disabled field in ajax callback', 2, function() {
- var form = $('form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'),
+ var form = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'),
+ input = form.find('input[type=submit]'),
newDisabledInput = input.clone().attr('disabled', 'disabled')
form.bindNative('ajax:success', function() {
@@ -238,9 +240,9 @@ asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:before` event
App.checkEnabledState(link, 'Click me')
link
- .bindNative('ajax:before', function() {
+ .bindNative('ajax:before', function(e) {
App.checkDisabledState(link, 'clicking...')
- return false
+ e.preventDefault()
})
.triggerNative('click')
@@ -256,9 +258,9 @@ asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:beforeSend` e
App.checkEnabledState(link, 'Click me')
link
- .bindNative('ajax:beforeSend', function() {
+ .bindNative('ajax:beforeSend', function(e) {
App.checkDisabledState(link, 'clicking...')
- return false
+ e.preventDefault()
})
.triggerNative('click')
@@ -293,8 +295,9 @@ asyncTest('form[data-remote] input|button|textarea[data-disable-with] does not d
submit = $('<input type="submit" data-disable-with="submitting ..." name="submit2" value="Submit" />').appendTo(form)
form
- .bindNative('ajax:beforeSend', function() {
- return false
+ .bindNative('ajax:beforeSend', function(e) {
+ e.preventDefault()
+ e.stopPropagation()
})
.triggerNative('submit')
@@ -343,9 +346,9 @@ asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:before`
App.checkEnabledState(button, 'Click me')
button
- .bindNative('ajax:before', function() {
+ .bindNative('ajax:before', function(e) {
App.checkDisabledState(button, 'clicking...')
- return false
+ e.preventDefault()
})
.triggerNative('click')
@@ -361,9 +364,9 @@ asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:beforeSe
App.checkEnabledState(button, 'Click me')
button
- .bindNative('ajax:beforeSend', function() {
+ .bindNative('ajax:beforeSend', function(e) {
App.checkDisabledState(button, 'clicking...')
- return false
+ e.preventDefault()
})
.triggerNative('click')
diff --git a/actionview/test/ujs/public/test/data-disable.js b/actionview/test/ujs/public/test/data-disable.js
index ccc38cf9ae..e9919764b6 100644
--- a/actionview/test/ujs/public/test/data-disable.js
+++ b/actionview/test/ujs/public/test/data-disable.js
@@ -91,7 +91,7 @@ asyncTest('form input[type=submit][data-disable] disables', 6, function() {
})
asyncTest('form[data-remote] input[type=submit][data-disable] is replaced in ajax callback', 2, function() {
- var form = $('form:not([data-remote])').attr('data-remote', 'true'), origFormContents = form.html()
+ var form = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'), origFormContents = form.html()
form.bindNative('ajax:success', function() {
form.html(origFormContents)
@@ -105,7 +105,7 @@ asyncTest('form[data-remote] input[type=submit][data-disable] is replaced in aja
})
asyncTest('form[data-remote] input[data-disable] is replaced with disabled field in ajax callback', 2, function() {
- var form = $('form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'),
+ var form = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'),
newDisabledInput = input.clone().attr('disabled', 'disabled')
form.bindNative('ajax:success', function() {
@@ -168,9 +168,9 @@ asyncTest('a[data-remote][data-disable] re-enables when `ajax:before` event is c
App.checkEnabledState(link, 'Click me')
link
- .bindNative('ajax:before', function() {
+ .bindNative('ajax:before', function(e) {
App.checkDisabledState(link, 'Click me')
- return false
+ e.preventDefault()
})
.triggerNative('click')
@@ -186,9 +186,9 @@ asyncTest('a[data-remote][data-disable] re-enables when `ajax:beforeSend` event
App.checkEnabledState(link, 'Click me')
link
- .bindNative('ajax:beforeSend', function() {
+ .bindNative('ajax:beforeSend', function(e) {
App.checkDisabledState(link, 'Click me')
- return false
+ e.preventDefault()
})
.triggerNative('click')
@@ -223,8 +223,9 @@ asyncTest('form[data-remote] input|button|textarea[data-disable] does not disabl
submit = $('<input type="submit" data-disable="submitting ..." name="submit2" value="Submit" />').appendTo(form)
form
- .bindNative('ajax:beforeSend', function() {
- return false
+ .bindNative('ajax:beforeSend', function(e) {
+ e.preventDefault()
+ e.stopPropagation()
})
.triggerNative('submit')
@@ -273,9 +274,9 @@ asyncTest('button[data-remote][data-disable] re-enables when `ajax:before` event
App.checkEnabledState(button, 'Click me')
button
- .bindNative('ajax:before', function() {
+ .bindNative('ajax:before', function(e) {
App.checkDisabledState(button, 'Click me')
- return false
+ e.preventDefault()
})
.triggerNative('click')
@@ -291,9 +292,9 @@ asyncTest('button[data-remote][data-disable] re-enables when `ajax:beforeSend` e
App.checkEnabledState(button, 'Click me')
button
- .bindNative('ajax:beforeSend', function() {
+ .bindNative('ajax:beforeSend', function(e) {
App.checkDisabledState(button, 'Click me')
- return false
+ e.preventDefault()
})
.triggerNative('click')
diff --git a/actionview/test/ujs/public/test/data-remote.js b/actionview/test/ujs/public/test/data-remote.js
index 9bbefc18f2..3503c2cff3 100644
--- a/actionview/test/ujs/public/test/data-remote.js
+++ b/actionview/test/ujs/public/test/data-remote.js
@@ -191,9 +191,10 @@ asyncTest('submitting form with data-remote attribute should include inputs in a
.triggerNative('submit')
})
-asyncTest('submitting form with data-remote attribute submits input with matching [form] attribute', 5, function() {
+asyncTest('submitting form with data-remote attribute submits input with matching [form] attribute', 6, function() {
$('#qunit-fixture')
.append($('<input type="text" name="user_data" value="value1" form="my-remote-form">'))
+ .append($('<input type="text" name="user_email" value="from@example.com" disabled="disabled" form="my-remote-form">'))
$('form[data-remote]')
.bindNative('ajax:success', function(e, data, status, xhr) {
@@ -201,6 +202,7 @@ asyncTest('submitting form with data-remote attribute submits input with matchin
App.assertRequestPath(data, '/echo')
equal(data.params.user_name, 'john', 'ajax arguments should have key user_name with right value')
equal(data.params.user_data, 'value1', 'ajax arguments should have key user_data with right value')
+ equal(data.params.user_email, undefined, 'ajax arguments should not have disabled field')
App.assertPostRequest(data)
})
.bindNative('ajax:complete', function() { start() })
@@ -270,9 +272,10 @@ asyncTest('returning false in form\'s submit bindings in non-submit-bubbling bro
form
.append($('<input type="submit" />'))
- .bindNative('submit', function() {
+ .bindNative('submit', function(e) {
ok(true, 'binding handler is called')
- return false
+ e.preventDefault()
+ e.stopPropagation()
})
.bindNative('ajax:beforeSend', function() {
ok(false, 'form should not be submitted')
@@ -294,8 +297,8 @@ asyncTest('clicking on a link with falsy "data-remote" attribute does not fire a
.bindNative('ajax:beforeSend', function() {
ok(false, 'ajax should not be triggered')
})
- .bindNative('click', function() {
- return false
+ .bindNative('click', function(e) {
+ e.preventDefault()
})
.triggerNative('click')
@@ -312,8 +315,8 @@ asyncTest('ctrl-clicking on a link with falsy "data-remote" attribute does not f
.bindNative('ajax:beforeSend', function() {
ok(false, 'ajax should not be triggered')
})
- .bindNative('click', function() {
- return false
+ .bindNative('click', function(e) {
+ e.preventDefault()
})
.triggerNative('click', { metaKey: true })
@@ -331,8 +334,8 @@ asyncTest('clicking on a button with falsy "data-remote" attribute', 0, function
.bindNative('ajax:beforeSend', function() {
ok(false, 'ajax should not be triggered')
})
- .bindNative('click', function() {
- return false
+ .bindNative('click', function(e) {
+ e.preventDefault()
})
.triggerNative('click')
@@ -345,8 +348,8 @@ asyncTest('submitting a form with falsy "data-remote" attribute', 0, function()
.bindNative('ajax:beforeSend', function() {
ok(false, 'ajax should not be triggered')
})
- .bindNative('submit', function() {
- return false
+ .bindNative('submit', function(e) {
+ e.preventDefault()
})
.triggerNative('submit')
@@ -427,7 +430,7 @@ asyncTest('changing a select option without "data-url" attribute still fires aja
ajaxLocation = settings.url.replace(settings.data, '').replace(/&$/, '').replace(/\?$/, '')
equal(ajaxLocation, currentLocation, 'URL should be current page by default')
- return false
+ e.preventDefault()
})
.val('optionValue2')
.triggerNative('change')
diff --git a/actionview/test/ujs/public/test/override.js b/actionview/test/ujs/public/test/override.js
index 299c7018cc..d73276ee4f 100644
--- a/actionview/test/ujs/public/test/override.js
+++ b/actionview/test/ujs/public/test/override.js
@@ -25,7 +25,7 @@ asyncTest('the getter for an element\'s href is overridable', 1, function() {
$('#qunit-fixture a')
.bindNative('ajax:beforeSend', function(e, xhr, options) {
equal('/data/href', options.url)
- return false
+ e.preventDefault()
})
.triggerNative('click')
start()
@@ -35,7 +35,7 @@ asyncTest('the getter for an element\'s href works normally if not overridden',
$('#qunit-fixture a')
.bindNative('ajax:beforeSend', function(e, xhr, options) {
equal(location.protocol + '//' + location.host + '/real/href', options.url)
- return false
+ e.preventDefault()
})
.triggerNative('click')
start()
diff --git a/actionview/test/ujs/public/test/settings.js b/actionview/test/ujs/public/test/settings.js
index 299c71bb00..b1ce3b8c64 100644
--- a/actionview/test/ujs/public/test/settings.js
+++ b/actionview/test/ujs/public/test/settings.js
@@ -103,14 +103,16 @@ $.fn.extend({
bindNative: function(event, handler) {
if (!handler) return this
- this.bind(event, function(e) {
+ var el = this[0]
+ el.addEventListener(event, function(e) {
var args = []
- if (e.originalEvent.detail) {
- args = e.originalEvent.detail.slice()
+ if (e.detail) {
+ args = e.detail.slice()
}
args.unshift(e)
- return handler.apply(this, args)
- })
+ return handler.apply(el, args)
+ }, false)
+
return this
}
})
diff --git a/actionview/test/ujs/server.rb b/actionview/test/ujs/server.rb
index 7d1bab4b2a..48e9bcb65f 100644
--- a/actionview/test/ujs/server.rb
+++ b/actionview/test/ujs/server.rb
@@ -23,18 +23,30 @@ module UJS
config.public_file_server.enabled = true
config.logger = Logger.new(STDOUT)
config.log_level = :error
+
+ config.content_security_policy do |policy|
+ policy.default_src :self, :https
+ policy.font_src :self, :https, :data
+ policy.img_src :self, :https, :data
+ policy.object_src :none
+ policy.script_src :self, :https
+ policy.style_src :self, :https
+ end
+
+ config.content_security_policy_nonce_generator = ->(req) { SecureRandom.base64(16) }
end
end
module TestsHelper
def test_to(*names)
- names = ["/vendor/qunit.js", "settings"] + names
- names.map { |name| script_tag name }.join("\n").html_safe
- end
+ names = names.map { |name| "/test/#{name}.js" }
+ names = %w[/vendor/qunit.js /test/settings.js] + names
- def script_tag(src)
- src = "/test/#{src}.js" unless src.index("/")
- %(<script src="#{src}" type="text/javascript"></script>).html_safe
+ capture do
+ names.each do |name|
+ concat(javascript_include_tag(name))
+ end
+ end
end
end
@@ -56,7 +68,7 @@ class TestsController < ActionController::Base
elsif params[:iframe]
payload = JSON.generate(data).gsub("<", "&lt;").gsub(">", "&gt;")
html = <<-HTML
- <script>
+ <script nonce="#{request.content_security_policy_nonce}">
if (window.top && window.top !== window)
window.top.jQuery.event.trigger('iframe:loaded', #{payload})
</script>
diff --git a/actionview/test/ujs/views/layouts/application.html.erb b/actionview/test/ujs/views/layouts/application.html.erb
index c787e77b84..8f6f6fc17f 100644
--- a/actionview/test/ujs/views/layouts/application.html.erb
+++ b/actionview/test/ujs/views/layouts/application.html.erb
@@ -2,9 +2,10 @@
<html id="html">
<head>
<title><%= @title %></title>
+ <%= csp_meta_tag %>
<link href="/vendor/qunit.css" media="screen" rel="stylesheet" type="text/css" media="screen, projection" />
<script src="/vendor/jquery-2.2.0.js" type="text/javascript"></script>
- <script>
+ <%= javascript_tag nonce: true do %>
// This is for test in override.js.
// Must go before rails-ujs.
document.addEventListener('rails:attachBindings', function() {
@@ -15,8 +16,8 @@
e.preventDefault();
});
});
- </script>
- <%= script_tag "/rails-ujs.js" %>
+ <% end %>
+ <%= javascript_include_tag "/rails-ujs.js" %>
</head>
<body id="body">
diff --git a/actionview/test/ujs/views/tests/index.html.erb b/actionview/test/ujs/views/tests/index.html.erb
index 8de6cd0695..6b16535216 100644
--- a/actionview/test/ujs/views/tests/index.html.erb
+++ b/actionview/test/ujs/views/tests/index.html.erb
@@ -1,6 +1,6 @@
<% @title = "rails-ujs test" %>
-<%= test_to 'data-confirm', 'data-remote', 'data-disable', 'data-disable-with', 'call-remote', 'call-remote-callbacks', 'data-method', 'override', 'csrf-refresh', 'csrf-token' %>
+<%= test_to 'data-confirm', 'data-remote', 'data-disable', 'data-disable-with', 'call-remote', 'call-remote-callbacks', 'data-method', 'override', 'csrf-refresh', 'csrf-token', 'call-ajax' %>
<h1 id="qunit-header"><%= @title %></h1>
<h2 id="qunit-banner"></h2>
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md
index 77dfdefc05..a3d13ad162 100644
--- a/activejob/CHANGELOG.md
+++ b/activejob/CHANGELOG.md
@@ -1,8 +1,26 @@
-* Change logging instrumentation to log errors when a job raises an exception.
+* Remove support for Qu gem.
- Fixes #26848.
+ Reasons are that the Qu gem wasn't compatible since Rails 5.1,
+ gem development was stopped in 2014 and maintainers have
+ confirmed its demise. See issue #32273
- *Steven Bull*
+ *Alberto Almagro*
+* Add support for timezones to Active Job.
-Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activejob/CHANGELOG.md) for previous changes.
+ Record what was the current timezone in effect when the job was
+ enqueued and then restore when the job is executed in same way
+ that the current locale is recorded and restored.
+
+ *Andrew White*
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
+
+* Add support to define custom argument serializers.
+
+ *Evgenii Pecherkin*, *Rafael Mendonça França*
+
+
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activejob/CHANGELOG.md) for previous changes.
diff --git a/activejob/MIT-LICENSE b/activejob/MIT-LICENSE
index daa726b9f0..274211f710 100644
--- a/activejob/MIT-LICENSE
+++ b/activejob/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2014-2017 David Heinemeier Hansson
+Copyright (c) 2014-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activejob/README.md b/activejob/README.md
index 8a9a23929b..d49fcfe3c2 100644
--- a/activejob/README.md
+++ b/activejob/README.md
@@ -88,6 +88,12 @@ Active Job has built-in adapters for multiple queueing backends (Sidekiq,
Resque, Delayed Job and others). To get an up-to-date list of the adapters
see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
+**Please note:** We are not accepting pull requests for new adapters. We
+encourage library authors to provide an ActiveJob adapter as part of
+their gem, or as a stand-alone gem. For discussion about this see the
+following PRs: [23311](https://github.com/rails/rails/issues/23311#issuecomment-176275718),
+[21406](https://github.com/rails/rails/pull/21406#issuecomment-138813484), and [#32285](https://github.com/rails/rails/pull/32285).
+
## Auxiliary gems
* [activejob-stats](https://github.com/seuros/activejob-stats)
@@ -100,7 +106,7 @@ The latest version of Active Job can be installed with RubyGems:
$ gem install activejob
```
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/activejob
@@ -117,7 +123,7 @@ API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/activejob/Rakefile b/activejob/Rakefile
index 6f13ef449d..b4da75adab 100644
--- a/activejob/Rakefile
+++ b/activejob/Rakefile
@@ -2,7 +2,6 @@
require "rake/testtask"
-#TODO: add qu back to the list after it support Rails 5.1
ACTIVEJOB_ADAPTERS = %w(async inline delayed_job que queue_classic resque sidekiq sneakers sucker_punch backburner test)
ACTIVEJOB_ADAPTERS.delete("queue_classic") if defined?(JRUBY_VERSION)
diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec
index 71e32f695b..be6292f737 100644
--- a/activejob/activejob.gemspec
+++ b/activejob/activejob.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Job framework with pluggable queues."
s.description = "Declare job classes that can be run by a variety of queueing backends."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb
index 56dab66544..01fab4d918 100644
--- a/activejob/lib/active_job.rb
+++ b/activejob/lib/active_job.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2014-2017 David Heinemeier Hansson
+# Copyright (c) 2014-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -25,7 +25,7 @@
require "active_support"
require "active_support/rails"
-require_relative "active_job/version"
+require "active_job/version"
require "global_id"
module ActiveJob
@@ -33,6 +33,7 @@ module ActiveJob
autoload :Base
autoload :QueueAdapters
+ autoload :Serializers
autoload :ConfiguredJob
autoload :TestCase
autoload :TestHelper
diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb
index de11e7fcb1..86bb0c5540 100644
--- a/activejob/lib/active_job/arguments.rb
+++ b/activejob/lib/active_job/arguments.rb
@@ -14,8 +14,8 @@ module ActiveJob
end
# Raised when an unsupported argument type is set as a job argument. We
- # currently support NilClass, Integer, Fixnum, Float, String, TrueClass, FalseClass,
- # Bignum, BigDecimal, and objects that can be represented as GlobalIDs (ex: Active Record).
+ # currently support NilClass, Integer, Float, String, TrueClass, FalseClass,
+ # BigDecimal, and objects that can be represented as GlobalIDs (ex: Active Record).
# Raised if you set the key for a Hash something else than a string or
# a symbol. Also raised when trying to serialize an object which can't be
# identified with a Global ID - such as an unpersisted Active Record model.
@@ -25,7 +25,6 @@ module ActiveJob
extend self
# :nodoc:
TYPE_WHITELIST = [ NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass ]
- TYPE_WHITELIST.push(Fixnum, Bignum) unless 1.class == Integer
# Serializes a set of arguments. Whitelisted types are returned
# as-is. Arrays/Hashes are serialized element by element.
@@ -44,13 +43,24 @@ module ActiveJob
end
private
+
# :nodoc:
GLOBALID_KEY = "_aj_globalid".freeze
# :nodoc:
SYMBOL_KEYS_KEY = "_aj_symbol_keys".freeze
# :nodoc:
WITH_INDIFFERENT_ACCESS_KEY = "_aj_hash_with_indifferent_access".freeze
- private_constant :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY
+ # :nodoc:
+ OBJECT_SERIALIZER_KEY = "_aj_serialized"
+
+ # :nodoc:
+ RESERVED_KEYS = [
+ GLOBALID_KEY, GLOBALID_KEY.to_sym,
+ SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym,
+ OBJECT_SERIALIZER_KEY, OBJECT_SERIALIZER_KEY.to_sym,
+ WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym,
+ ]
+ private_constant :RESERVED_KEYS
def serialize_argument(argument)
case argument
@@ -70,7 +80,7 @@ module ActiveJob
result[SYMBOL_KEYS_KEY] = symbol_keys
result
else
- raise SerializationError.new("Unsupported argument type: #{argument.class.name}")
+ Serializers.serialize(argument)
end
end
@@ -85,6 +95,8 @@ module ActiveJob
when Hash
if serialized_global_id?(argument)
deserialize_global_id argument
+ elsif custom_serialized?(argument)
+ Serializers.deserialize(argument)
else
deserialize_hash(argument)
end
@@ -101,6 +113,10 @@ module ActiveJob
GlobalID::Locator.locate hash[GLOBALID_KEY]
end
+ def custom_serialized?(hash)
+ hash.key?(OBJECT_SERIALIZER_KEY)
+ end
+
def serialize_hash(argument)
argument.each_with_object({}) do |(key, value), hash|
hash[serialize_hash_key(key)] = serialize_argument(value)
@@ -117,14 +133,6 @@ module ActiveJob
result
end
- # :nodoc:
- RESERVED_KEYS = [
- GLOBALID_KEY, GLOBALID_KEY.to_sym,
- SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym,
- WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym,
- ]
- private_constant :RESERVED_KEYS
-
def serialize_hash_key(key)
case key
when *RESERVED_KEYS
diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb
index 6af41260db..2b2a59e969 100644
--- a/activejob/lib/active_job/base.rb
+++ b/activejob/lib/active_job/base.rb
@@ -1,15 +1,16 @@
# frozen_string_literal: true
-require_relative "core"
-require_relative "queue_adapter"
-require_relative "queue_name"
-require_relative "queue_priority"
-require_relative "enqueuing"
-require_relative "execution"
-require_relative "callbacks"
-require_relative "exceptions"
-require_relative "logging"
-require_relative "translation"
+require "active_job/core"
+require "active_job/queue_adapter"
+require "active_job/queue_name"
+require "active_job/queue_priority"
+require "active_job/enqueuing"
+require "active_job/execution"
+require "active_job/callbacks"
+require "active_job/exceptions"
+require "active_job/logging"
+require "active_job/timezones"
+require "active_job/translation"
module ActiveJob #:nodoc:
# = Active Job
@@ -59,6 +60,7 @@ module ActiveJob #:nodoc:
# * SerializationError - Error class for serialization errors.
class Base
include Core
+ include Serializers
include QueueAdapter
include QueueName
include QueuePriority
@@ -67,6 +69,7 @@ module ActiveJob #:nodoc:
include Callbacks
include Exceptions
include Logging
+ include Timezones
include Translation
ActiveSupport.run_load_hooks(:active_job, self)
diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb
index c4e12fc518..da841ae45b 100644
--- a/activejob/lib/active_job/core.rb
+++ b/activejob/lib/active_job/core.rb
@@ -31,6 +31,9 @@ module ActiveJob
# I18n.locale to be used during the job.
attr_accessor :locale
+
+ # Timezone to be used during the job.
+ attr_accessor :timezone
end
# These methods will be included into any Active Job object, adding
@@ -87,7 +90,8 @@ module ActiveJob
"priority" => priority,
"arguments" => serialize_arguments(arguments),
"executions" => executions,
- "locale" => I18n.locale.to_s
+ "locale" => I18n.locale.to_s,
+ "timezone" => Time.zone.try(:name)
}
end
@@ -97,17 +101,23 @@ module ActiveJob
# ==== Examples
#
# class DeliverWebhookJob < ActiveJob::Base
+ # attr_writer :attempt_number
+ #
+ # def attempt_number
+ # @attempt_number ||= 0
+ # end
+ #
# def serialize
- # super.merge('attempt_number' => (@attempt_number || 0) + 1)
+ # super.merge('attempt_number' => attempt_number + 1)
# end
#
# def deserialize(job_data)
# super
- # @attempt_number = job_data['attempt_number']
+ # self.attempt_number = job_data['attempt_number']
# end
#
- # rescue_from(TimeoutError) do |exception|
- # raise exception if @attempt_number > 5
+ # rescue_from(Timeout::Error) do |exception|
+ # raise exception if attempt_number > 5
# retry_job(wait: 10)
# end
# end
@@ -119,6 +129,7 @@ module ActiveJob
self.serialized_arguments = job_data["arguments"]
self.executions = job_data["executions"]
self.locale = job_data["locale"] || I18n.locale.to_s
+ self.timezone = job_data["timezone"] || Time.zone.try(:name)
end
private
diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb
index c0d016853c..53cb98fc71 100644
--- a/activejob/lib/active_job/enqueuing.rb
+++ b/activejob/lib/active_job/enqueuing.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "arguments"
+require "active_job/arguments"
module ActiveJob
# Provides behavior for enqueuing jobs.
diff --git a/activejob/lib/active_job/exceptions.rb b/activejob/lib/active_job/exceptions.rb
index dfc74deb1a..ae700848d0 100644
--- a/activejob/lib/active_job/exceptions.rb
+++ b/activejob/lib/active_job/exceptions.rb
@@ -49,7 +49,7 @@ module ActiveJob
retry_job wait: determine_delay(wait), queue: queue, priority: priority
else
if block_given?
- yield self, exception
+ yield self, error
else
logger.error "Stopped retrying #{self.class} due to a #{exception}, which reoccurred on #{executions} attempts. The original exception was #{error.cause.inspect}."
raise error
@@ -61,18 +61,28 @@ module ActiveJob
# Discard the job with no attempts to retry, if the exception is raised. This is useful when the subject of the job,
# like an Active Record, is no longer available, and the job is thus no longer relevant.
#
+ # You can also pass a block that'll be invoked. This block is yielded with the job instance as the first and the error instance as the second parameter.
+ #
# ==== Example
#
# class SearchIndexingJob < ActiveJob::Base
# discard_on ActiveJob::DeserializationError
+ # discard_on(CustomAppException) do |job, exception|
+ # ExceptionNotifier.caught(exception)
+ # end
#
# def perform(record)
# # Will raise ActiveJob::DeserializationError if the record can't be deserialized
+ # # Might raise CustomAppException for something domain specific
# end
# end
def discard_on(exception)
rescue_from exception do |error|
- logger.error "Discarded #{self.class} due to a #{exception}. The original exception was #{error.cause.inspect}."
+ if block_given?
+ yield self, exception
+ else
+ logger.error "Discarded #{self.class} due to a #{exception}. The original exception was #{error.cause.inspect}."
+ end
end
end
end
diff --git a/activejob/lib/active_job/execution.rb b/activejob/lib/active_job/execution.rb
index 85e050b489..d75be376ec 100644
--- a/activejob/lib/active_job/execution.rb
+++ b/activejob/lib/active_job/execution.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "active_support/rescuable"
-require_relative "arguments"
+require "active_job/arguments"
module ActiveJob
module Execution
diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb
index 7ee61780e1..770f70dc5e 100644
--- a/activejob/lib/active_job/gem_version.rb
+++ b/activejob/lib/active_job/gem_version.rb
@@ -7,8 +7,8 @@ module ActiveJob
end
module VERSION
- MAJOR = 5
- MINOR = 2
+ MAJOR = 6
+ MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb
index f53b7eaee5..3312857ac7 100644
--- a/activejob/lib/active_job/logging.rb
+++ b/activejob/lib/active_job/logging.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/hash/transform_values"
require "active_support/core_ext/string/filters"
require "active_support/tagged_logging"
require "active_support/logger"
@@ -12,13 +11,13 @@ module ActiveJob
included do
cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
- around_enqueue do |_, block, _|
+ around_enqueue do |_, block|
tag_logger do
block.call
end
end
- around_perform do |job, block, _|
+ around_perform do |job, block|
tag_logger(job.class.name, job.job_id) do
payload = { adapter: job.class.queue_adapter, job: job }
ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup)
diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb
index dd05800baf..006a683b85 100644
--- a/activejob/lib/active_job/queue_adapter.rb
+++ b/activejob/lib/active_job/queue_adapter.rb
@@ -29,28 +29,22 @@ module ActiveJob
# Specify the backend queue provider. The default queue adapter
# is the +:async+ queue. See QueueAdapters for more
# information.
- def queue_adapter=(name_or_adapter_or_class)
- interpret_adapter(name_or_adapter_or_class)
- end
-
- private
-
- def interpret_adapter(name_or_adapter_or_class)
- case name_or_adapter_or_class
- when Symbol, String
- assign_adapter(name_or_adapter_or_class.to_s,
- ActiveJob::QueueAdapters.lookup(name_or_adapter_or_class).new)
+ def queue_adapter=(name_or_adapter)
+ case name_or_adapter
+ when Symbol, String
+ queue_adapter = ActiveJob::QueueAdapters.lookup(name_or_adapter).new
+ assign_adapter(name_or_adapter.to_s, queue_adapter)
+ else
+ if queue_adapter?(name_or_adapter)
+ adapter_name = "#{name_or_adapter.class.name.demodulize.remove('Adapter').underscore}"
+ assign_adapter(adapter_name, name_or_adapter)
else
- if queue_adapter?(name_or_adapter_or_class)
- adapter_name = "#{name_or_adapter_or_class.class.name.demodulize.remove('Adapter').underscore}"
- assign_adapter(adapter_name,
- name_or_adapter_or_class)
- else
- raise ArgumentError
- end
+ raise ArgumentError
end
end
+ end
+ private
def assign_adapter(adapter_name, queue_adapter)
self._queue_adapter_name = adapter_name
self._queue_adapter = queue_adapter
diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb
index c1a1d3c510..7854467cc1 100644
--- a/activejob/lib/active_job/queue_adapters.rb
+++ b/activejob/lib/active_job/queue_adapters.rb
@@ -7,7 +7,6 @@ module ActiveJob
#
# * {Backburner}[https://github.com/nesquena/backburner]
# * {Delayed Job}[https://github.com/collectiveidea/delayed_job]
- # * {Qu}[https://github.com/bkeepers/qu]
# * {Que}[https://github.com/chanks/que]
# * {queue_classic}[https://github.com/QueueClassic/queue_classic]
# * {Resque}[https://github.com/resque/resque]
@@ -16,6 +15,7 @@ module ActiveJob
# * {Sucker Punch}[https://github.com/brandonhilkert/sucker_punch]
# * {Active Job Async Job}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html]
# * {Active Job Inline}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/InlineAdapter.html]
+ # * Please Note: We are not accepting pull requests for new adapters. See the README for more details.
#
# === Backends Features
#
@@ -23,7 +23,6 @@ module ActiveJob
# |-------------------|-------|--------|------------|------------|---------|---------|
# | Backburner | Yes | Yes | Yes | Yes | Job | Global |
# | Delayed Job | Yes | Yes | Yes | Job | Global | Global |
- # | Qu | Yes | Yes | No | No | No | Global |
# | Que | Yes | Yes | Yes | Job | No | Job |
# | queue_classic | Yes | Yes | Yes* | No | No | No |
# | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes |
@@ -114,7 +113,6 @@ module ActiveJob
autoload :InlineAdapter
autoload :BackburnerAdapter
autoload :DelayedJobAdapter
- autoload :QuAdapter
autoload :QueAdapter
autoload :QueueClassicAdapter
autoload :ResqueAdapter
diff --git a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
index 1978179948..8eeef32b99 100644
--- a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
@@ -34,6 +34,10 @@ module ActiveJob
@job_data = job_data
end
+ def display_name
+ "#{job_data['job_class']} [#{job_data['job_id']}] from DelayedJob(#{job_data['queue_name']}) with arguments: #{job_data['arguments']}"
+ end
+
def perform
Base.execute(job_data)
end
diff --git a/activejob/lib/active_job/queue_adapters/qu_adapter.rb b/activejob/lib/active_job/queue_adapters/qu_adapter.rb
deleted file mode 100644
index bd7003e177..0000000000
--- a/activejob/lib/active_job/queue_adapters/qu_adapter.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-require "qu"
-
-module ActiveJob
- module QueueAdapters
- # == Qu adapter for Active Job
- #
- # Qu is a Ruby library for queuing and processing background jobs. It is
- # heavily inspired by delayed_job and Resque. Qu was created to overcome
- # some shortcomings in the existing queuing libraries.
- # The advantages of Qu are: Multiple backends (redis, mongo), jobs are
- # requeued when worker is killed, resque-like API.
- #
- # Read more about Qu {here}[https://github.com/bkeepers/qu].
- #
- # To use Qu set the queue_adapter config to +:qu+.
- #
- # Rails.application.config.active_job.queue_adapter = :qu
- class QuAdapter
- def enqueue(job, *args) #:nodoc:
- qu_job = Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload|
- payload.instance_variable_set(:@queue, job.queue_name)
- end.push
-
- # qu_job can be nil depending on the configured backend
- job.provider_job_id = qu_job.id unless qu_job.nil?
- qu_job
- end
-
- def enqueue_at(job, timestamp, *args) #:nodoc:
- raise NotImplementedError, "This queueing backend does not support scheduling jobs. To see what features are supported go to http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html"
- end
-
- class JobWrapper < Qu::Job #:nodoc:
- def initialize(job_data)
- @job_data = job_data
- end
-
- def perform
- Base.execute @job_data
- end
- end
- end
- end
-end
diff --git a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb
index 5a1135854b..f726e6ad93 100644
--- a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb
@@ -18,7 +18,7 @@ module ActiveJob
# Rails.application.config.active_job.queue_adapter = :sidekiq
class SidekiqAdapter
def enqueue(job) #:nodoc:
- #Sidekiq::Client does not support symbols as keys
+ # Sidekiq::Client does not support symbols as keys
job.provider_job_id = Sidekiq::Client.push \
"class" => JobWrapper,
"wrapped" => job.class.to_s,
diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb
index 7b0742a6d2..d0294854d3 100644
--- a/activejob/lib/active_job/railtie.rb
+++ b/activejob/lib/active_job/railtie.rb
@@ -7,17 +7,28 @@ module ActiveJob
# = Active Job Railtie
class Railtie < Rails::Railtie # :nodoc:
config.active_job = ActiveSupport::OrderedOptions.new
+ config.active_job.custom_serializers = []
initializer "active_job.logger" do
ActiveSupport.on_load(:active_job) { self.logger = ::Rails.logger }
end
+ initializer "active_job.custom_serializers" do |app|
+ config.after_initialize do
+ custom_serializers = app.config.active_job.delete(:custom_serializers)
+ ActiveJob::Serializers.add_serializers custom_serializers
+ end
+ end
+
initializer "active_job.set_configs" do |app|
options = app.config.active_job
options.queue_adapter ||= :async
ActiveSupport.on_load(:active_job) do
- options.each { |k, v| send("#{k}=", v) }
+ options.each do |k, v|
+ k = "#{k}="
+ send(k, v) if respond_to? k
+ end
end
end
diff --git a/activejob/lib/active_job/serializers.rb b/activejob/lib/active_job/serializers.rb
new file mode 100644
index 0000000000..df66e66659
--- /dev/null
+++ b/activejob/lib/active_job/serializers.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require "set"
+
+module ActiveJob
+ # The <tt>ActiveJob::Serializers</tt> module is used to store a list of known serializers
+ # and to add new ones. It also has helpers to serialize/deserialize objects.
+ module Serializers # :nodoc:
+ extend ActiveSupport::Autoload
+ extend ActiveSupport::Concern
+
+ autoload :ObjectSerializer
+ autoload :SymbolSerializer
+ autoload :DurationSerializer
+ autoload :DateTimeSerializer
+ autoload :DateSerializer
+ autoload :TimeWithZoneSerializer
+ autoload :TimeSerializer
+
+ mattr_accessor :_additional_serializers
+ self._additional_serializers = Set.new
+
+ class << self
+ # Returns serialized representative of the passed object.
+ # Will look up through all known serializers.
+ # Raises <tt>ActiveJob::SerializationError</tt> if it can't find a proper serializer.
+ def serialize(argument)
+ serializer = serializers.detect { |s| s.serialize?(argument) }
+ raise SerializationError.new("Unsupported argument type: #{argument.class.name}") unless serializer
+ serializer.serialize(argument)
+ end
+
+ # Returns deserialized object.
+ # Will look up through all known serializers.
+ # If no serializer found will raise <tt>ArgumentError</tt>.
+ def deserialize(argument)
+ serializer_name = argument[Arguments::OBJECT_SERIALIZER_KEY]
+ raise ArgumentError, "Serializer name is not present in the argument: #{argument.inspect}" unless serializer_name
+
+ serializer = serializer_name.safe_constantize
+ raise ArgumentError, "Serializer #{serializer_name} is not known" unless serializer
+
+ serializer.deserialize(argument)
+ end
+
+ # Returns list of known serializers.
+ def serializers
+ self._additional_serializers
+ end
+
+ # Adds new serializers to a list of known serializers.
+ def add_serializers(*new_serializers)
+ self._additional_serializers += new_serializers.flatten
+ end
+ end
+
+ add_serializers SymbolSerializer,
+ DurationSerializer,
+ DateTimeSerializer,
+ DateSerializer,
+ TimeWithZoneSerializer,
+ TimeSerializer
+ end
+end
diff --git a/activejob/lib/active_job/serializers/date_serializer.rb b/activejob/lib/active_job/serializers/date_serializer.rb
new file mode 100644
index 0000000000..e995d30faa
--- /dev/null
+++ b/activejob/lib/active_job/serializers/date_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class DateSerializer < ObjectSerializer # :nodoc:
+ def serialize(date)
+ super("value" => date.iso8601)
+ end
+
+ def deserialize(hash)
+ Date.iso8601(hash["value"])
+ end
+
+ private
+
+ def klass
+ Date
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/date_time_serializer.rb b/activejob/lib/active_job/serializers/date_time_serializer.rb
new file mode 100644
index 0000000000..fe780a1978
--- /dev/null
+++ b/activejob/lib/active_job/serializers/date_time_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class DateTimeSerializer < ObjectSerializer # :nodoc:
+ def serialize(time)
+ super("value" => time.iso8601)
+ end
+
+ def deserialize(hash)
+ DateTime.iso8601(hash["value"])
+ end
+
+ private
+
+ def klass
+ DateTime
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/duration_serializer.rb b/activejob/lib/active_job/serializers/duration_serializer.rb
new file mode 100644
index 0000000000..715fe27a5c
--- /dev/null
+++ b/activejob/lib/active_job/serializers/duration_serializer.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class DurationSerializer < ObjectSerializer # :nodoc:
+ def serialize(duration)
+ super("value" => duration.value, "parts" => Arguments.serialize(duration.parts))
+ end
+
+ def deserialize(hash)
+ value = hash["value"]
+ parts = Arguments.deserialize(hash["parts"])
+
+ klass.new(value, parts)
+ end
+
+ private
+
+ def klass
+ ActiveSupport::Duration
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/object_serializer.rb b/activejob/lib/active_job/serializers/object_serializer.rb
new file mode 100644
index 0000000000..6d280969be
--- /dev/null
+++ b/activejob/lib/active_job/serializers/object_serializer.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ # Base class for serializing and deserializing custom objects.
+ #
+ # Example:
+ #
+ # class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
+ # def serialize(money)
+ # super("amount" => money.amount, "currency" => money.currency)
+ # end
+ #
+ # def deserialize(hash)
+ # Money.new(hash["amount"], hash["currency"])
+ # end
+ #
+ # private
+ #
+ # def klass
+ # Money
+ # end
+ # end
+ class ObjectSerializer
+ include Singleton
+
+ class << self
+ delegate :serialize?, :serialize, :deserialize, to: :instance
+ end
+
+ # Determines if an argument should be serialized by a serializer.
+ def serialize?(argument)
+ argument.is_a?(klass)
+ end
+
+ # Serializes an argument to a JSON primitive type.
+ def serialize(hash)
+ { Arguments::OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash)
+ end
+
+ # Deserializes an argument from a JSON primitive type.
+ def deserialize(_argument)
+ raise NotImplementedError
+ end
+
+ private
+
+ # The class of the object that will be serialized.
+ def klass # :doc:
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/symbol_serializer.rb b/activejob/lib/active_job/serializers/symbol_serializer.rb
new file mode 100644
index 0000000000..7e1f9553a2
--- /dev/null
+++ b/activejob/lib/active_job/serializers/symbol_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class SymbolSerializer < ObjectSerializer # :nodoc:
+ def serialize(argument)
+ super("value" => argument.to_s)
+ end
+
+ def deserialize(argument)
+ argument["value"].to_sym
+ end
+
+ private
+
+ def klass
+ Symbol
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/time_serializer.rb b/activejob/lib/active_job/serializers/time_serializer.rb
new file mode 100644
index 0000000000..fe20772f35
--- /dev/null
+++ b/activejob/lib/active_job/serializers/time_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class TimeSerializer < ObjectSerializer # :nodoc:
+ def serialize(time)
+ super("value" => time.iso8601)
+ end
+
+ def deserialize(hash)
+ Time.iso8601(hash["value"])
+ end
+
+ private
+
+ def klass
+ Time
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/time_with_zone_serializer.rb b/activejob/lib/active_job/serializers/time_with_zone_serializer.rb
new file mode 100644
index 0000000000..43017fc75b
--- /dev/null
+++ b/activejob/lib/active_job/serializers/time_with_zone_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class TimeWithZoneSerializer < ObjectSerializer # :nodoc:
+ def serialize(time)
+ super("value" => time.iso8601)
+ end
+
+ def deserialize(hash)
+ Time.iso8601(hash["value"]).in_time_zone
+ end
+
+ private
+
+ def klass
+ ActiveSupport::TimeWithZone
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/timezones.rb b/activejob/lib/active_job/timezones.rb
new file mode 100644
index 0000000000..ac018eb752
--- /dev/null
+++ b/activejob/lib/active_job/timezones.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Timezones #:nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ around_perform do |job, block|
+ Time.use_zone(job.timezone, &block)
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/translation.rb b/activejob/lib/active_job/translation.rb
index fb45c80d67..0fd9b9fc06 100644
--- a/activejob/lib/active_job/translation.rb
+++ b/activejob/lib/active_job/translation.rb
@@ -5,7 +5,7 @@ module ActiveJob
extend ActiveSupport::Concern
included do
- around_perform do |job, block, _|
+ around_perform do |job, block|
I18n.with_locale(job.locale, &block)
end
end
diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb
index 69b4fe7d26..03346a7f12 100644
--- a/activejob/lib/rails/generators/job/job_generator.rb
+++ b/activejob/lib/rails/generators/job/job_generator.rb
@@ -28,6 +28,10 @@ module Rails # :nodoc:
end
private
+ def file_name
+ @_file_name ||= super.sub(/_job\z/i, "")
+ end
+
def application_job_file_name
@application_job_file_name ||= if mountable_engine?
"app/jobs/#{namespaced_path}/application_job.rb"
diff --git a/activejob/lib/rails/generators/job/templates/application_job.rb b/activejob/lib/rails/generators/job/templates/application_job.rb.tt
index f93745a31a..f93745a31a 100644
--- a/activejob/lib/rails/generators/job/templates/application_job.rb
+++ b/activejob/lib/rails/generators/job/templates/application_job.rb.tt
diff --git a/activejob/lib/rails/generators/job/templates/job.rb b/activejob/lib/rails/generators/job/templates/job.rb.tt
index 4ad2914a45..4ad2914a45 100644
--- a/activejob/lib/rails/generators/job/templates/job.rb
+++ b/activejob/lib/rails/generators/job/templates/job.rb.tt
diff --git a/activejob/test/adapters/qu.rb b/activejob/test/adapters/qu.rb
deleted file mode 100644
index 5b471fa347..0000000000
--- a/activejob/test/adapters/qu.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-require "qu-immediate"
-
-ActiveJob::Base.queue_adapter = :qu
diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb
index 13e6fcb727..e5f1f087fe 100644
--- a/activejob/test/cases/argument_serialization_test.rb
+++ b/activejob/test/cases/argument_serialization_test.rb
@@ -12,7 +12,10 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
end
[ nil, 1, 1.0, 1_000_000_000_000_000_000_000,
- "a", true, false, BigDecimal.new(5),
+ "a", true, false, BigDecimal(5),
+ :a, 1.day, Date.new(2001, 2, 3), Time.new(2002, 10, 31, 2, 2, 2, "+02:00"),
+ DateTime.new(2001, 2, 3, 4, 5, 6, "+03:00"),
+ ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]),
[ 1, "a" ],
{ "a" => 1 }
].each do |arg|
@@ -21,7 +24,7 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
end
end
- [ :a, Object.new, self, Person.find("5").to_gid ].each do |arg|
+ [ Object.new, self, Person.find("5").to_gid ].each do |arg|
test "does not serialize #{arg.class}" do
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [ arg ]
@@ -46,6 +49,49 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
assert_arguments_roundtrip([a: 1, "b" => 2])
end
+ test "serialize a hash" do
+ symbol_key = { a: 1 }
+ string_key = { "a" => 1 }
+ indifferent_access = { a: 1 }.with_indifferent_access
+
+ assert_equal(
+ { "a" => 1, "_aj_symbol_keys" => ["a"] },
+ ActiveJob::Arguments.serialize([symbol_key]).first
+ )
+ assert_equal(
+ { "a" => 1, "_aj_symbol_keys" => [] },
+ ActiveJob::Arguments.serialize([string_key]).first
+ )
+ assert_equal(
+ { "a" => 1, "_aj_hash_with_indifferent_access" => true },
+ ActiveJob::Arguments.serialize([indifferent_access]).first
+ )
+ end
+
+ test "deserialize a hash" do
+ symbol_key = { "a" => 1, "_aj_symbol_keys" => ["a"] }
+ string_key = { "a" => 1, "_aj_symbol_keys" => [] }
+ another_string_key = { "a" => 1 }
+ indifferent_access = { "a" => 1, "_aj_hash_with_indifferent_access" => true }
+
+ assert_equal(
+ { a: 1 },
+ ActiveJob::Arguments.deserialize([symbol_key]).first
+ )
+ assert_equal(
+ { "a" => 1 },
+ ActiveJob::Arguments.deserialize([string_key]).first
+ )
+ assert_equal(
+ { "a" => 1 },
+ ActiveJob::Arguments.deserialize([another_string_key]).first
+ )
+ assert_equal(
+ { "a" => 1 },
+ ActiveJob::Arguments.deserialize([indifferent_access]).first
+ )
+ end
+
test "should maintain hash with indifferent access" do
symbol_key = { a: 1 }
string_key = { "a" => 1 }
@@ -56,6 +102,14 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
assert_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([indifferent_access]).first
end
+ test "should maintain time with zone" do
+ Time.use_zone "Alaska" do
+ time_with_zone = Time.new(2002, 10, 31, 2, 2, 2).in_time_zone
+ assert_instance_of ActiveSupport::TimeWithZone, perform_round_trip([time_with_zone]).first
+ assert_arguments_unchanged time_with_zone
+ end
+ end
+
test "should disallow non-string/symbol hash keys" do
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [ { 1 => 2 } ]
diff --git a/activejob/test/cases/exceptions_test.rb b/activejob/test/cases/exceptions_test.rb
index 7a3c372143..bc33d79f61 100644
--- a/activejob/test/cases/exceptions_test.rb
+++ b/activejob/test/cases/exceptions_test.rb
@@ -58,10 +58,17 @@ class ExceptionsTest < ActiveJob::TestCase
end
end
+ test "custom handling of discarded job" do
+ perform_enqueued_jobs do
+ RetryJob.perform_later "CustomDiscardableError", 2
+ assert_equal "Dealt with a job that was discarded in a custom way", JobBuffer.last_value
+ end
+ end
+
test "custom handling of job that exceeds retry attempts" do
perform_enqueued_jobs do
RetryJob.perform_later "CustomCatchError", 6
- assert_equal "Dealt with a job that failed to retry in a custom way after 6 attempts", JobBuffer.last_value
+ assert_equal "Dealt with a job that failed to retry in a custom way after 6 attempts. Message: CustomCatchError", JobBuffer.last_value
end
end
diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb
index 440051c427..5c9994508e 100644
--- a/activejob/test/cases/job_serialization_test.rb
+++ b/activejob/test/cases/job_serialization_test.rb
@@ -54,4 +54,11 @@ class JobSerializationTest < ActiveSupport::TestCase
job.provider_job_id = "some value set by adapter"
assert_equal job.provider_job_id, job.serialize["provider_job_id"]
end
+
+ test "serialize stores the current timezone" do
+ Time.use_zone "Hawaii" do
+ job = HelloJob.new
+ assert_equal "Hawaii", job.serialize["timezone"]
+ end
+ end
end
diff --git a/activejob/test/cases/serializers_test.rb b/activejob/test/cases/serializers_test.rb
new file mode 100644
index 0000000000..bee0c061bd
--- /dev/null
+++ b/activejob/test/cases/serializers_test.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require "helper"
+require "active_job/serializers"
+
+class SerializersTest < ActiveSupport::TestCase
+ class DummyValueObject
+ attr_accessor :value
+
+ def initialize(value)
+ @value = value
+ end
+
+ def ==(other)
+ self.value == other.value
+ end
+ end
+
+ class DummySerializer < ActiveJob::Serializers::ObjectSerializer
+ def serialize(object)
+ super({ "value" => object.value })
+ end
+
+ def deserialize(hash)
+ DummyValueObject.new(hash["value"])
+ end
+
+ private
+
+ def klass
+ DummyValueObject
+ end
+ end
+
+ setup do
+ @value_object = DummyValueObject.new 123
+ @original_serializers = ActiveJob::Serializers.serializers
+ end
+
+ teardown do
+ ActiveJob::Serializers._additional_serializers = @original_serializers
+ end
+
+ test "can't serialize unknown object" do
+ assert_raises ActiveJob::SerializationError do
+ ActiveJob::Serializers.serialize @value_object
+ end
+ end
+
+ test "will serialize objects with serializers registered" do
+ ActiveJob::Serializers.add_serializers DummySerializer
+
+ assert_equal(
+ { "_aj_serialized" => "SerializersTest::DummySerializer", "value" => 123 },
+ ActiveJob::Serializers.serialize(@value_object)
+ )
+ end
+
+ test "won't deserialize unknown hash" do
+ hash = { "_dummy_serializer" => 123, "_aj_symbol_keys" => [] }
+ error = assert_raises(ArgumentError) do
+ ActiveJob::Serializers.deserialize(hash)
+ end
+ assert_equal(
+ 'Serializer name is not present in the argument: {"_dummy_serializer"=>123, "_aj_symbol_keys"=>[]}',
+ error.message
+ )
+ end
+
+ test "won't deserialize unknown serializer" do
+ hash = { "_aj_serialized" => "DoNotExist", "value" => 123 }
+ error = assert_raises(ArgumentError) do
+ ActiveJob::Serializers.deserialize(hash)
+ end
+ assert_equal(
+ "Serializer DoNotExist is not known",
+ error.message
+ )
+ end
+
+ test "will deserialize know serialized objects" do
+ ActiveJob::Serializers.add_serializers DummySerializer
+ hash = { "_aj_serialized" => "SerializersTest::DummySerializer", "value" => 123 }
+ assert_equal DummyValueObject.new(123), ActiveJob::Serializers.deserialize(hash)
+ end
+
+ test "adds new serializer" do
+ ActiveJob::Serializers.add_serializers DummySerializer
+ assert ActiveJob::Serializers.serializers.include?(DummySerializer)
+ end
+
+ test "can't add serializer with the same key twice" do
+ ActiveJob::Serializers.add_serializers DummySerializer
+ assert_no_difference(-> { ActiveJob::Serializers.serializers.size }) do
+ ActiveJob::Serializers.add_serializers DummySerializer
+ end
+ end
+end
diff --git a/activejob/test/cases/timezones_test.rb b/activejob/test/cases/timezones_test.rb
new file mode 100644
index 0000000000..e2095b020d
--- /dev/null
+++ b/activejob/test/cases/timezones_test.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "helper"
+require "jobs/timezone_dependent_job"
+
+class TimezonesTest < ActiveSupport::TestCase
+ setup do
+ JobBuffer.clear
+ end
+
+ test "it performs the job in the given timezone" do
+ job = TimezoneDependentJob.new("2018-01-01T00:00:00Z")
+ job.timezone = "London"
+ job.perform_now
+
+ assert_equal "Happy New Year!", JobBuffer.last_value
+
+ job = TimezoneDependentJob.new("2018-01-01T00:00:00Z")
+ job.timezone = "Eastern Time (US & Canada)"
+ job.perform_now
+
+ assert_equal "Just 5 hours to go", JobBuffer.last_value
+ end
+end
diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb
index 0d8aa336a6..32afb5ca62 100644
--- a/activejob/test/integration/queuing_test.rb
+++ b/activejob/test/integration/queuing_test.rb
@@ -45,6 +45,13 @@ class QueuingTest < ActiveSupport::TestCase
end
end
+ test "should supply a wrapped class name to DelayedJob" do
+ skip unless adapter_is?(:delayed_job)
+ ::HelloJob.perform_later
+ job = Delayed::Job.first
+ assert_match(/HelloJob \[[0-9a-f-]+\] from DelayedJob\(default\) with arguments: \[\]/, job.name)
+ end
+
test "resque JobWrapper should have instance variable queue" do
skip unless adapter_is?(:resque)
job = ::HelloJob.set(wait: 5.seconds).perform_later
@@ -75,7 +82,7 @@ class QueuingTest < ActiveSupport::TestCase
end
test "should supply a provider_job_id when available for immediate jobs" do
- skip unless adapter_is?(:async, :delayed_job, :sidekiq, :qu, :que, :queue_classic)
+ skip unless adapter_is?(:async, :delayed_job, :sidekiq, :que, :queue_classic)
test_job = TestJob.perform_later @id
assert test_job.provider_job_id, "Provider job id should be set by provider"
end
@@ -103,6 +110,22 @@ class QueuingTest < ActiveSupport::TestCase
end
end
+ test "current timezone is kept while running perform_later" do
+ skip if adapter_is?(:inline)
+
+ begin
+ current_zone = Time.zone
+ Time.zone = "Hawaii"
+
+ TestJob.perform_later @id
+ wait_for_jobs_to_finish_for(5.seconds)
+ assert job_executed
+ assert_equal "Hawaii", job_executed_in_timezone
+ ensure
+ Time.zone = current_zone
+ end
+ end
+
test "should run job with higher priority first" do
skip unless adapter_is?(:delayed_job, :que)
diff --git a/activejob/test/jobs/retry_job.rb b/activejob/test/jobs/retry_job.rb
index a12d09779b..82b80fe12b 100644
--- a/activejob/test/jobs/retry_job.rb
+++ b/activejob/test/jobs/retry_job.rb
@@ -10,6 +10,7 @@ class ExponentialWaitTenAttemptsError < StandardError; end
class CustomWaitTenAttemptsError < StandardError; end
class CustomCatchError < StandardError; end
class DiscardableError < StandardError; end
+class CustomDiscardableError < StandardError; end
class RetryJob < ActiveJob::Base
retry_on DefaultsError
@@ -17,8 +18,9 @@ class RetryJob < ActiveJob::Base
retry_on ShortWaitTenAttemptsError, wait: 1.second, attempts: 10
retry_on ExponentialWaitTenAttemptsError, wait: :exponentially_longer, attempts: 10
retry_on CustomWaitTenAttemptsError, wait: ->(executions) { executions * 2 }, attempts: 10
- retry_on(CustomCatchError) { |job, exception| JobBuffer.add("Dealt with a job that failed to retry in a custom way after #{job.arguments.second} attempts") }
+ retry_on(CustomCatchError) { |job, exception| JobBuffer.add("Dealt with a job that failed to retry in a custom way after #{job.arguments.second} attempts. Message: #{exception.message}") }
discard_on DiscardableError
+ discard_on(CustomDiscardableError) { |job, exception| JobBuffer.add("Dealt with a job that was discarded in a custom way") }
def perform(raising, attempts)
if executions < attempts
diff --git a/activejob/test/jobs/timezone_dependent_job.rb b/activejob/test/jobs/timezone_dependent_job.rb
new file mode 100644
index 0000000000..41f473d533
--- /dev/null
+++ b/activejob/test/jobs/timezone_dependent_job.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
+
+class TimezoneDependentJob < ActiveJob::Base
+ def perform(now)
+ now = now.in_time_zone
+ new_year = localtime(2018, 1, 1)
+
+ if now >= new_year
+ JobBuffer.add("Happy New Year!")
+ else
+ JobBuffer.add("Just #{(new_year - now).div(3600)} hours to go")
+ end
+ end
+
+ private
+
+ def localtime(*args)
+ Time.zone ? Time.zone.local(*args) : Time.utc(*args)
+ end
+end
diff --git a/activejob/test/support/delayed_job/delayed/backend/test.rb b/activejob/test/support/delayed_job/delayed/backend/test.rb
index 4721c1cc17..1691896b7c 100644
--- a/activejob/test/support/delayed_job/delayed/backend/test.rb
+++ b/activejob/test/support/delayed_job/delayed/backend/test.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-#copied from https://github.com/collectiveidea/delayed_job/blob/master/spec/delayed/backend/test.rb
+# copied from https://github.com/collectiveidea/delayed_job/blob/master/spec/delayed/backend/test.rb
require "ostruct"
# An in-memory backend suitable only for testing. Tries to behave as if it were an ORM.
@@ -77,7 +77,7 @@ module Delayed
self.locked_by = worker
end
- return true
+ true
end
def self.db_time_now
diff --git a/activejob/test/support/integration/adapters/qu.rb b/activejob/test/support/integration/adapters/qu.rb
deleted file mode 100644
index 67db03e279..0000000000
--- a/activejob/test/support/integration/adapters/qu.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module QuJobsManager
- def setup
- require "qu-rails"
- require "qu-redis"
- ActiveJob::Base.queue_adapter = :qu
- ENV["REDISTOGO_URL"] = "redis://127.0.0.1:6379/12"
- backend = Qu::Backend::Redis.new
- backend.namespace = "active_jobs_int_test"
- Qu.backend = backend
- Qu.logger = Rails.logger
- Qu.interval = 0.5
- unless can_run?
- puts "Cannot run integration tests for qu. To be able to run integration tests for qu you need to install and start redis.\n"
- exit
- end
- end
-
- def clear_jobs
- Qu.clear "integration_tests"
- end
-
- def start_workers
- @thread = Thread.new { Qu::Worker.new("integration_tests").start }
- end
-
- def stop_workers
- @thread.kill
- end
-
- def can_run?
- begin
- Qu.backend.connection.client.connect
- rescue
- return false
- end
- true
- end
-end
diff --git a/activejob/test/support/integration/adapters/resque.rb b/activejob/test/support/integration/adapters/resque.rb
index 484b476567..2ed8302277 100644
--- a/activejob/test/support/integration/adapters/resque.rb
+++ b/activejob/test/support/integration/adapters/resque.rb
@@ -3,11 +3,12 @@
module ResqueJobsManager
def setup
ActiveJob::Base.queue_adapter = :resque
- Resque.redis = Redis::Namespace.new "active_jobs_int_test", redis: Redis.connect(url: "redis://:password@127.0.0.1:6379/12", thread_safe: true)
+ Resque.redis = Redis::Namespace.new "active_jobs_int_test", redis: Redis.new(url: "redis://127.0.0.1:6379/12", thread_safe: true)
Resque.logger = Rails.logger
unless can_run?
puts "Cannot run integration tests for resque. To be able to run integration tests for resque you need to install and start redis.\n"
- exit
+ status = ENV["CI"] ? false : true
+ exit status
end
end
@@ -41,11 +42,8 @@ module ResqueJobsManager
end
def can_run?
- begin
- Resque.redis.client.connect
- rescue
- return false
- end
- true
+ Resque.redis.ping == "PONG"
+ rescue
+ false
end
end
diff --git a/activejob/test/support/integration/adapters/sidekiq.rb b/activejob/test/support/integration/adapters/sidekiq.rb
index ceb7fb61f2..c79de12eaf 100644
--- a/activejob/test/support/integration/adapters/sidekiq.rb
+++ b/activejob/test/support/integration/adapters/sidekiq.rb
@@ -5,20 +5,13 @@ require "sidekiq/api"
require "sidekiq/testing"
Sidekiq::Testing.disable!
-Sidekiq.configure_server do |config|
- config.redis = { url: "redis://:password@127.0.0.1:6379/12" }
-end
-
-Sidekiq.configure_client do |config|
- config.redis = { url: "redis://:password@127.0.0.1:6379/12" }
-end
-
module SidekiqJobsManager
def setup
ActiveJob::Base.queue_adapter = :sidekiq
unless can_run?
puts "Cannot run integration tests for sidekiq. To be able to run integration tests for sidekiq you need to install and start redis.\n"
- exit
+ status = ENV["CI"] ? false : true
+ exit status
end
end
@@ -58,6 +51,7 @@ module SidekiqJobsManager
self_write.puts("TERM")
end
+ require "sidekiq/cli"
require "sidekiq/launcher"
sidekiq = Sidekiq::Launcher.new(queues: ["integration_tests"],
environment: "test",
diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb
index ac382bd1b7..b56dd3e591 100644
--- a/activejob/test/support/integration/dummy_app_template.rb
+++ b/activejob/test/support/integration/dummy_app_template.rb
@@ -4,8 +4,6 @@ if ENV["AJ_ADAPTER"] == "delayed_job"
generate "delayed_job:active_record", "--quiet"
end
-rails_command("db:migrate")
-
initializer "activejob.rb", <<-CODE
require "#{File.expand_path("jobs_manager.rb", __dir__)}"
JobsManager.current_manager.setup
@@ -23,6 +21,7 @@ class TestJob < ActiveJob::Base
File.open(Rails.root.join("tmp/\#{x}.new"), "wb+") do |f|
f.write Marshal.dump({
"locale" => I18n.locale.to_s || "en",
+ "timezone" => Time.zone.try(:name) || "UTC",
"executed_at" => Time.now.to_r
})
end
diff --git a/activejob/test/support/integration/helper.rb b/activejob/test/support/integration/helper.rb
index a058da141f..a02d874e2e 100644
--- a/activejob/test/support/integration/helper.rb
+++ b/activejob/test/support/integration/helper.rb
@@ -18,6 +18,7 @@ Rails::Generators::AppGenerator.start args
require "#{dummy_app_path}/config/environment.rb"
ActiveRecord::Migrator.migrations_paths = [ Rails.root.join("db/migrate").to_s ]
+ActiveRecord::Tasks::DatabaseTasks.migrate
require "rails/test_help"
Rails.backtrace_cleaner.remove_silencers!
diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb
index f02a32a38e..3d9b265b66 100644
--- a/activejob/test/support/integration/test_case_helpers.rb
+++ b/activejob/test/support/integration/test_case_helpers.rb
@@ -62,4 +62,8 @@ module TestCaseHelpers
def job_executed_in_locale(id = @id)
job_data(id)["locale"]
end
+
+ def job_executed_in_timezone(id = @id)
+ job_data(id)["timezone"]
+ end
end
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 048c43f2c4..6b557a7cb1 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,41 +1,6 @@
-* Add method `#merge!` for `ActiveModel::Errors`.
+* Rails 6 requires Ruby 2.4.1 or newer.
- *Jahfer Husain*
+ *Jeremy Daer*
-* Fix regression in numericality validator when comparing Decimal and Float input
- values with more scale than the schema.
- *Bradley Priest*
-
-* Fix methods `#keys`, `#values` in `ActiveModel::Errors`.
-
- Change `#keys` to only return the keys that don't have empty messages.
-
- Change `#values` to only return the not empty values.
-
- Example:
-
- # Before
- person = Person.new
- person.errors.keys # => []
- person.errors.values # => []
- person.errors.messages # => {}
- person.errors[:name] # => []
- person.errors.messages # => {:name => []}
- person.errors.keys # => [:name]
- person.errors.values # => [[]]
-
- # After
- person = Person.new
- person.errors.keys # => []
- person.errors.values # => []
- person.errors.messages # => {}
- person.errors[:name] # => []
- person.errors.messages # => {:name => []}
- person.errors.keys # => []
- person.errors.values # => []
-
- *bogdanvlviv*
-
-
-Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activemodel/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activemodel/CHANGELOG.md) for previous changes.
diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE
index ac810e86d0..1cb3add0fc 100644
--- a/activemodel/MIT-LICENSE
+++ b/activemodel/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2017 David Heinemeier Hansson
+Copyright (c) 2004-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc
index 772df0f8f6..1aaf4813ea 100644
--- a/activemodel/README.rdoc
+++ b/activemodel/README.rdoc
@@ -251,11 +251,11 @@ Active Model is released under the MIT license:
== Support
-API documentation is at
+API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec
index a070a2898c..7be466dc4c 100644
--- a/activemodel/activemodel.gemspec
+++ b/activemodel/activemodel.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "A toolkit for building modeling frameworks (part of Rails)."
s.description = "A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index dfd9be34a9..bc10d6b4b9 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2004-2017 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -25,11 +25,13 @@
require "active_support"
require "active_support/rails"
-require_relative "active_model/version"
+require "active_model/version"
module ActiveModel
extend ActiveSupport::Autoload
+ autoload :Attribute
+ autoload :Attributes
autoload :AttributeAssignment
autoload :AttributeMethods
autoload :BlockValidator, "active_model/validator"
@@ -45,6 +47,7 @@ module ActiveModel
autoload :SecurePassword
autoload :Serialization
autoload :Translation
+ autoload :Type
autoload :Validations
autoload :Validator
diff --git a/activerecord/lib/active_record/attribute.rb b/activemodel/lib/active_model/attribute.rb
index fc474edc15..3f19cda07b 100644
--- a/activerecord/lib/active_record/attribute.rb
+++ b/activemodel/lib/active_model/attribute.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
-module ActiveRecord
+require "active_support/core_ext/object/duplicable"
+
+module ActiveModel
class Attribute # :nodoc:
class << self
def from_database(name, value, type)
@@ -130,13 +132,7 @@ module ActiveRecord
coder["value"] = value if defined?(@value)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
protected
-
- attr_reader :original_attribute
- alias_method :assigned?, :original_attribute
-
def original_value_for_database
if assigned?
original_attribute.original_value_for_database
@@ -146,6 +142,9 @@ module ActiveRecord
end
private
+ attr_reader :original_attribute
+ alias :assigned? :original_attribute
+
def initialize_dup(other)
if defined?(@value) && @value.duplicable?
@value = @value.dup
@@ -233,10 +232,15 @@ module ActiveRecord
false
end
+ def forgetting_assignment
+ dup
+ end
+
def with_type(type)
self.class.new(name, type)
end
end
+
private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
end
end
diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activemodel/lib/active_model/attribute/user_provided_default.rb
index 690a931615..9dc16e882d 100644
--- a/activerecord/lib/active_record/attribute/user_provided_default.rb
+++ b/activemodel/lib/active_model/attribute/user_provided_default.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "../attribute"
+require "active_model/attribute"
-module ActiveRecord
+module ActiveModel
class Attribute # :nodoc:
class UserProvidedDefault < FromUser # :nodoc:
def initialize(name, value, type, database_default)
@@ -22,10 +22,29 @@ module ActiveRecord
self.class.new(name, user_provided_value, type, original_attribute)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ def marshal_dump
+ result = [
+ name,
+ value_before_type_cast,
+ type,
+ original_attribute,
+ ]
+ result << value if defined?(@value)
+ result
+ end
+
+ def marshal_load(values)
+ name, user_provided_value, type, original_attribute, value = values
+ @name = name
+ @user_provided_value = user_provided_value
+ @type = type
+ @original_attribute = original_attribute
+ if values.length == 5
+ @value = value
+ end
+ end
+ private
attr_reader :user_provided_value
end
end
diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb
index aa931119ff..217bf1ac01 100644
--- a/activemodel/lib/active_model/attribute_assignment.rb
+++ b/activemodel/lib/active_model/attribute_assignment.rb
@@ -35,6 +35,8 @@ module ActiveModel
_assign_attributes(sanitize_for_mass_assignment(attributes))
end
+ alias attributes= assign_attributes
+
private
def _assign_attributes(attributes)
diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb
index 94bf641a5d..6abf37bd44 100644
--- a/activerecord/lib/active_record/attribute_mutation_tracker.rb
+++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
-module ActiveRecord
+require "active_support/core_ext/hash/indifferent_access"
+
+module ActiveModel
class AttributeMutationTracker # :nodoc:
OPTION_NOT_GIVEN = Object.new
@@ -9,6 +11,10 @@ module ActiveRecord
@forced_changes = Set.new
end
+ def changed_attribute_names
+ attr_names.select { |attr_name| changed?(attr_name) }
+ end
+
def changed_values
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
if changed?(attr_name)
@@ -21,7 +27,7 @@ module ActiveRecord
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
change = change_to_attribute(attr_name)
if change
- result[attr_name] = change
+ result.merge!(attr_name => change)
end
end
end
@@ -63,13 +69,8 @@ module ActiveRecord
forced_changes << attr_name.to_s
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :attributes, :forced_changes
-
private
+ attr_reader :attributes, :forced_changes
def attr_names
attributes.keys
@@ -79,6 +80,10 @@ module ActiveRecord
class NullMutationTracker # :nodoc:
include Singleton
+ def changed_attribute_names(*)
+ []
+ end
+
def changed_values(*)
{}
end
@@ -107,5 +112,8 @@ module ActiveRecord
def original_value(*)
end
+
+ def force_change(*)
+ end
end
end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activemodel/lib/active_model/attribute_set.rb
index 492067e2b3..a890ee3932 100644
--- a/activerecord/lib/active_record/attribute_set.rb
+++ b/activemodel/lib/active_model/attribute_set.rb
@@ -1,11 +1,12 @@
# frozen_string_literal: true
-require_relative "attribute_set/builder"
-require_relative "attribute_set/yaml_encoder"
+require "active_support/core_ext/object/deep_dup"
+require "active_model/attribute_set/builder"
+require "active_model/attribute_set/yaml_encoder"
-module ActiveRecord
+module ActiveModel
class AttributeSet # :nodoc:
- delegate :each_value, :fetch, to: :attributes
+ delegate :each_value, :fetch, :except, to: :attributes
def initialize(attributes)
@attributes = attributes
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activemodel/lib/active_model/attribute_set/builder.rb
index e3a9c7fdb3..2b1c2206ec 100644
--- a/activerecord/lib/active_record/attribute_set/builder.rb
+++ b/activemodel/lib/active_model/attribute_set/builder.rb
@@ -1,39 +1,34 @@
# frozen_string_literal: true
-require_relative "../attribute"
+require "active_model/attribute"
-module ActiveRecord
+module ActiveModel
class AttributeSet # :nodoc:
class Builder # :nodoc:
- attr_reader :types, :always_initialized, :default
+ attr_reader :types, :default_attributes
- def initialize(types, always_initialized = nil, &default)
+ def initialize(types, default_attributes = {})
@types = types
- @always_initialized = always_initialized
- @default = default
+ @default_attributes = default_attributes
end
def build_from_database(values = {}, additional_types = {})
- if always_initialized && !values.key?(always_initialized)
- values[always_initialized] = nil
- end
-
- attributes = LazyAttributeHash.new(types, values, additional_types, &default)
+ attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
AttributeSet.new(attributes)
end
end
end
class LazyAttributeHash # :nodoc:
- delegate :transform_values, :each_key, :each_value, :fetch, to: :materialize
+ delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize
- def initialize(types, values, additional_types, &default)
+ def initialize(types, values, additional_types, default_attributes, delegate_hash = {})
@types = types
@values = values
@additional_types = additional_types
@materialized = false
- @delegate_hash = {}
- @default = default || proc {}
+ @delegate_hash = delegate_hash
+ @default_attributes = default_attributes
end
def key?(key)
@@ -81,23 +76,20 @@ module ActiveRecord
end
def marshal_dump
- materialize
+ [@types, @values, @additional_types, @default_attributes, @delegate_hash]
end
- def marshal_load(delegate_hash)
- @delegate_hash = delegate_hash
- @types = {}
- @values = {}
- @additional_types = {}
- @materialized = true
+ def marshal_load(values)
+ if values.is_a?(Hash)
+ empty_hash = {}.freeze
+ initialize(empty_hash, empty_hash, empty_hash, empty_hash, values)
+ @materialized = true
+ else
+ initialize(*values)
+ end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
protected
-
- attr_reader :types, :values, :additional_types, :delegate_hash, :default
-
def materialize
unless @materialized
values.each_key { |key| self[key] }
@@ -110,6 +102,7 @@ module ActiveRecord
end
private
+ attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
def assign_default_value(name)
type = additional_types.fetch(name, types[name])
@@ -119,7 +112,12 @@ module ActiveRecord
if value_present
delegate_hash[name] = Attribute.from_database(name, value, type)
elsif types.key?(name)
- delegate_hash[name] = default.call(name) || Attribute.uninitialized(name, type)
+ attr = default_attributes[name]
+ if attr
+ delegate_hash[name] = attr.dup
+ else
+ delegate_hash[name] = Attribute.uninitialized(name, type)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb
index 9254ce16ab..ea1efc160e 100644
--- a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb
+++ b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-module ActiveRecord
+module ActiveModel
class AttributeSet
# Attempts to do more intelligent YAML dumping of an
- # ActiveRecord::AttributeSet to reduce the size of the resulting string
+ # ActiveModel::AttributeSet to reduce the size of the resulting string
class YAMLEncoder # :nodoc:
def initialize(default_types)
@default_types = default_types
@@ -33,10 +33,7 @@ module ActiveRecord
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :default_types
end
end
diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb
new file mode 100644
index 0000000000..7d44f7f2a3
--- /dev/null
+++ b/activemodel/lib/active_model/attributes.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require "active_model/attribute_set"
+require "active_model/attribute/user_provided_default"
+
+module ActiveModel
+ module Attributes #:nodoc:
+ extend ActiveSupport::Concern
+ include ActiveModel::AttributeMethods
+
+ included do
+ attribute_method_suffix "="
+ class_attribute :attribute_types, :_default_attributes, instance_accessor: false
+ self.attribute_types = Hash.new(Type.default_value)
+ self._default_attributes = AttributeSet.new({})
+ end
+
+ module ClassMethods
+ def attribute(name, type = Type::Value.new, **options)
+ name = name.to_s
+ if type.is_a?(Symbol)
+ type = ActiveModel::Type.lookup(type, **options.except(:default))
+ end
+ self.attribute_types = attribute_types.merge(name => type)
+ define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type)
+ define_attribute_method(name)
+ end
+
+ private
+
+ def define_method_attribute=(name)
+ safe_name = name.unpack1("h*".freeze)
+ ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name
+
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__#{safe_name}=(value)
+ name = ::ActiveModel::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ write_attribute(name, value)
+ end
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
+ undef_method :__temp__#{safe_name}=
+ STR
+ end
+
+ NO_DEFAULT_PROVIDED = Object.new # :nodoc:
+ private_constant :NO_DEFAULT_PROVIDED
+
+ def define_default_attribute(name, value, type)
+ self._default_attributes = _default_attributes.deep_dup
+ if value == NO_DEFAULT_PROVIDED
+ default_attribute = _default_attributes[name].with_type(type)
+ else
+ default_attribute = Attribute::UserProvidedDefault.new(
+ name,
+ value,
+ type,
+ _default_attributes.fetch(name.to_s) { nil },
+ )
+ end
+ _default_attributes[name] = default_attribute
+ end
+ end
+
+ def initialize(*)
+ @attributes = self.class._default_attributes.deep_dup
+ super
+ end
+
+ def attributes
+ @attributes.to_hash
+ end
+
+ private
+
+ def write_attribute(attr_name, value)
+ name = if self.class.attribute_alias?(attr_name)
+ self.class.attribute_alias(attr_name).to_s
+ else
+ attr_name.to_s
+ end
+
+ @attributes.write_from_user(name, value)
+ value
+ end
+
+ def attribute(attr_name)
+ name = if self.class.attribute_alias?(attr_name)
+ self.class.attribute_alias(attr_name).to_s
+ else
+ attr_name.to_s
+ end
+ @attributes.fetch_value(name)
+ end
+
+ # Handle *= for method_missing.
+ def attribute=(attribute_name, value)
+ write_attribute(attribute_name, value)
+ end
+ end
+
+ module AttributeMethods #:nodoc:
+ AttrNames = Module.new {
+ def self.set_name_cache(name, value)
+ const_name = "ATTR_#{name}"
+ unless const_defined? const_name
+ const_set const_name, value.dup.freeze
+ end
+ end
+ }
+ end
+end
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 943db0ab52..eaf8dfb223 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -2,6 +2,7 @@
require "active_support/hash_with_indifferent_access"
require "active_support/core_ext/object/duplicable"
+require "active_model/attribute_mutation_tracker"
module ActiveModel
# == Active \Model \Dirty
@@ -130,6 +131,26 @@ module ActiveModel
attribute_method_affix prefix: "restore_", suffix: "!"
end
+ def initialize_dup(other) # :nodoc:
+ super
+ if self.class.respond_to?(:_default_attributes)
+ @attributes = self.class._default_attributes.map do |attr|
+ attr.with_value_from_user(@attributes.fetch_value(attr.name))
+ end
+ end
+ @mutations_from_database = nil
+ end
+
+ def changes_applied # :nodoc:
+ unless defined?(@attributes)
+ @previously_changed = changes
+ end
+ @mutations_before_last_save = mutations_from_database
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
+ forget_attribute_assignments
+ @mutations_from_database = nil
+ end
+
# Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
#
# person.changed? # => false
@@ -148,6 +169,60 @@ module ActiveModel
changed_attributes.keys
end
+ # Handles <tt>*_changed?</tt> for +method_missing+.
+ def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
+ !!changes_include?(attr) &&
+ (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
+ (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
+ end
+
+ # Handles <tt>*_was</tt> for +method_missing+.
+ def attribute_was(attr) # :nodoc:
+ attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
+ end
+
+ # Handles <tt>*_previously_changed?</tt> for +method_missing+.
+ def attribute_previously_changed?(attr) #:nodoc:
+ previous_changes_include?(attr)
+ end
+
+ # Restore all previous data of the provided attributes.
+ def restore_attributes(attributes = changed)
+ attributes.each { |attr| restore_attribute! attr }
+ end
+
+ # Clears all dirty data: current changes and previous changes.
+ def clear_changes_information
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
+ @mutations_before_last_save = nil
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
+ forget_attribute_assignments
+ @mutations_from_database = nil
+ end
+
+ def clear_attribute_changes(attr_names)
+ attributes_changed_by_setter.except!(*attr_names)
+ attr_names.each do |attr_name|
+ clear_attribute_change(attr_name)
+ end
+ end
+
+ # Returns a hash of the attributes with unsaved changes indicating their original
+ # values like <tt>attr => original value</tt>.
+ #
+ # person.name # => "bob"
+ # person.name = 'robert'
+ # person.changed_attributes # => {"name" => "bob"}
+ def changed_attributes
+ # This should only be set by methods which will call changed_attributes
+ # multiple times when it is known that the computed value cannot change.
+ if defined?(@cached_changed_attributes)
+ @cached_changed_attributes
+ else
+ attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze
+ end
+ end
+
# Returns a hash of changed attributes indicating their original
# and new values like <tt>attr => [original value, new value]</tt>.
#
@@ -155,7 +230,9 @@ module ActiveModel
# person.name = 'bob'
# person.changes # => { "name" => ["bill", "bob"] }
def changes
- ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
+ cache_changed_attributes do
+ ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
+ end
end
# Returns a hash of attributes that were changed before the model was saved.
@@ -166,45 +243,51 @@ module ActiveModel
# person.previous_changes # => {"name" => ["bob", "robert"]}
def previous_changes
@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
+ @previously_changed.merge(mutations_before_last_save.changes)
end
- # Returns a hash of the attributes with unsaved changes indicating their original
- # values like <tt>attr => original value</tt>.
- #
- # person.name # => "bob"
- # person.name = 'robert'
- # person.changed_attributes # => {"name" => "bob"}
- def changed_attributes
- @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
+ def attribute_changed_in_place?(attr_name) # :nodoc:
+ mutations_from_database.changed_in_place?(attr_name)
end
- # Handles <tt>*_changed?</tt> for +method_missing+.
- def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
- !!changes_include?(attr) &&
- (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
- (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
- end
+ private
+ def clear_attribute_change(attr_name)
+ mutations_from_database.forget_change(attr_name)
+ end
- # Handles <tt>*_was</tt> for +method_missing+.
- def attribute_was(attr) # :nodoc:
- attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
- end
+ def mutations_from_database
+ unless defined?(@mutations_from_database)
+ @mutations_from_database = nil
+ end
+ @mutations_from_database ||= if defined?(@attributes)
+ ActiveModel::AttributeMutationTracker.new(@attributes)
+ else
+ NullMutationTracker.instance
+ end
+ end
- # Handles <tt>*_previously_changed?</tt> for +method_missing+.
- def attribute_previously_changed?(attr) #:nodoc:
- previous_changes_include?(attr)
- end
+ def forget_attribute_assignments
+ @attributes = @attributes.map(&:forgetting_assignment) if defined?(@attributes)
+ end
- # Restore all previous data of the provided attributes.
- def restore_attributes(attributes = changed)
- attributes.each { |attr| restore_attribute! attr }
- end
+ def mutations_before_last_save
+ @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
+ end
- private
+ def cache_changed_attributes
+ @cached_changed_attributes = changed_attributes
+ yield
+ ensure
+ clear_changed_attributes_cache
+ end
+
+ def clear_changed_attributes_cache
+ remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
+ end
# Returns +true+ if attr_name is changed, +false+ otherwise.
def changes_include?(attr_name)
- attributes_changed_by_setter.include?(attr_name)
+ attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
end
alias attribute_changed_by_setter? changes_include?
@@ -214,18 +297,6 @@ module ActiveModel
previous_changes.include?(attr_name)
end
- # Removes current changes and makes them accessible through +previous_changes+.
- def changes_applied # :doc:
- @previously_changed = changes
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- end
-
- # Clears all dirty data: current changes and previous changes.
- def clear_changes_information # :doc:
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- end
-
# Handles <tt>*_change</tt> for +method_missing+.
def attribute_change(attr)
[changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
@@ -238,15 +309,16 @@ module ActiveModel
# Handles <tt>*_will_change!</tt> for +method_missing+.
def attribute_will_change!(attr)
- return if attribute_changed?(attr)
+ unless attribute_changed?(attr)
+ begin
+ value = _read_attribute(attr)
+ value = value.duplicable? ? value.clone : value
+ rescue TypeError, NoMethodError
+ end
- begin
- value = _read_attribute(attr)
- value = value.duplicable? ? value.clone : value
- rescue TypeError, NoMethodError
+ set_attribute_was(attr, value)
end
-
- set_attribute_was(attr, value)
+ mutations_from_database.force_change(attr)
end
# Handles <tt>restore_*!</tt> for +method_missing+.
@@ -257,18 +329,13 @@ module ActiveModel
end
end
- # This is necessary because `changed_attributes` might be overridden in
- # other implementations (e.g. in `ActiveRecord`)
- alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
+ def attributes_changed_by_setter
+ @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
+ end
# Force an attribute to have a particular "before" value
def set_attribute_was(attr, old_value)
attributes_changed_by_setter[attr] = old_value
end
-
- # Remove changes information for the provided attributes.
- def clear_attribute_changes(attributes) # :doc:
- attributes_changed_by_setter.except!(*attributes)
- end
end
end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 971bdd08b1..275e3f1313 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -322,9 +322,13 @@ module ActiveModel
# person.errors.added? :name, :too_long # => false
# person.errors.added? :name, "is too long" # => false
def added?(attribute, message = :invalid, options = {})
- message = message.call if message.respond_to?(:call)
- message = normalize_message(attribute, message, options)
- self[attribute].include? message
+ if message.is_a? Symbol
+ self.details[attribute].map { |e| e[:error] }.include? message
+ else
+ message = message.call if message.respond_to?(:call)
+ message = normalize_message(attribute, message, options)
+ self[attribute].include? message
+ end
end
# Returns all the full error messages in an array.
diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
index 39269c159c..cef5441e4a 100644
--- a/activemodel/lib/active_model/gem_version.rb
+++ b/activemodel/lib/active_model/gem_version.rb
@@ -7,8 +7,8 @@ module ActiveModel
end
module VERSION
- MAJOR = 5
- MINOR = 2
+ MAJOR = 6
+ MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb
index 34d9ac6c96..b7ceabb59a 100644
--- a/activemodel/lib/active_model/lint.rb
+++ b/activemodel/lib/active_model/lint.rb
@@ -29,7 +29,7 @@ module ActiveModel
# <tt>to_key</tt> returns an Enumerable of all (primary) key attributes
# of the model, and is used to a generate unique DOM id for the object.
def test_to_key
- assert model.respond_to?(:to_key), "The model should respond to to_key"
+ assert_respond_to model, :to_key
def model.persisted?() false end
assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false"
end
@@ -44,7 +44,7 @@ module ActiveModel
# tests for this behavior in lint because it doesn't make sense to force
# any of the possible implementation strategies on the implementer.
def test_to_param
- assert model.respond_to?(:to_param), "The model should respond to to_param"
+ assert_respond_to model, :to_param
def model.to_key() [1] end
def model.persisted?() false end
assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
@@ -56,7 +56,7 @@ module ActiveModel
# <tt>to_partial_path</tt> is used for looking up partials. For example,
# a BlogPost model might return "blog_posts/blog_post".
def test_to_partial_path
- assert model.respond_to?(:to_partial_path), "The model should respond to to_partial_path"
+ assert_respond_to model, :to_partial_path
assert_kind_of String, model.to_partial_path
end
@@ -68,7 +68,7 @@ module ActiveModel
# will route to the create action. If it is persisted, a form for the
# object will route to the update action.
def test_persisted?
- assert model.respond_to?(:persisted?), "The model should respond to persisted?"
+ assert_respond_to model, :persisted?
assert_boolean model.persisted?, "persisted?"
end
@@ -79,14 +79,14 @@ module ActiveModel
#
# Check ActiveModel::Naming for more information.
def test_model_naming
- assert model.class.respond_to?(:model_name), "The model class should respond to model_name"
+ assert_respond_to model.class, :model_name
model_name = model.class.model_name
- assert model_name.respond_to?(:to_str)
- assert model_name.human.respond_to?(:to_str)
- assert model_name.singular.respond_to?(:to_str)
- assert model_name.plural.respond_to?(:to_str)
+ assert_respond_to model_name, :to_str
+ assert_respond_to model_name.human, :to_str
+ assert_respond_to model_name.singular, :to_str
+ assert_respond_to model_name.plural, :to_str
- assert model.respond_to?(:model_name), "The model instance should respond to model_name"
+ assert_respond_to model, :model_name
assert_equal model.model_name, model.class.model_name
end
@@ -100,13 +100,13 @@ module ActiveModel
# If localization is used, the strings should be localized for the current
# locale. If no error is present, the method should return an empty array.
def test_errors_aref
- assert model.respond_to?(:errors), "The model should respond to errors"
+ assert_respond_to model, :errors
assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array"
end
private
def model
- assert @model.respond_to?(:to_model), "The object should respond to to_model"
+ assert_respond_to @model, :to_model
@model.to_model
end
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
index 47cb81bee5..c4b7b32291 100644
--- a/activemodel/lib/active_model/serialization.rb
+++ b/activemodel/lib/active_model/serialization.rb
@@ -179,7 +179,7 @@ module ActiveModel
return unless includes = options[:include]
unless includes.is_a?(Hash)
- includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
+ includes = Hash[Array(includes).flat_map { |n| n.is_a?(Hash) ? n.to_a : [[n, {}]] }]
end
includes.each do |association, opts|
diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb
index cb603b3d25..1d7a26fff5 100644
--- a/activemodel/lib/active_model/type.rb
+++ b/activemodel/lib/active_model/type.rb
@@ -1,21 +1,21 @@
# frozen_string_literal: true
-require_relative "type/helpers"
-require_relative "type/value"
-
-require_relative "type/big_integer"
-require_relative "type/binary"
-require_relative "type/boolean"
-require_relative "type/date"
-require_relative "type/date_time"
-require_relative "type/decimal"
-require_relative "type/float"
-require_relative "type/immutable_string"
-require_relative "type/integer"
-require_relative "type/string"
-require_relative "type/time"
-
-require_relative "type/registry"
+require "active_model/type/helpers"
+require "active_model/type/value"
+
+require "active_model/type/big_integer"
+require "active_model/type/binary"
+require "active_model/type/boolean"
+require "active_model/type/date"
+require "active_model/type/date_time"
+require "active_model/type/decimal"
+require "active_model/type/float"
+require "active_model/type/immutable_string"
+require "active_model/type/integer"
+require "active_model/type/string"
+require "active_model/type/time"
+
+require "active_model/type/registry"
module ActiveModel
module Type
@@ -24,7 +24,7 @@ module ActiveModel
class << self
attr_accessor :registry # :nodoc:
- # Add a new type to the registry, allowing it to be get through ActiveModel::Type#lookup
+ # Add a new type to the registry, allowing it to be gotten through ActiveModel::Type#lookup
def register(type_name, klass = nil, **options, &block)
registry.register(type_name, klass, **options, &block)
end
@@ -32,6 +32,10 @@ module ActiveModel
def lookup(*args, **kwargs) # :nodoc:
registry.lookup(*args, **kwargs)
end
+
+ def default_value # :nodoc:
+ @default_value ||= Value.new
+ end
end
register(:big_integer, Type::BigInteger)
diff --git a/activemodel/lib/active_model/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb
index d080fcc0f2..89e43bcc5f 100644
--- a/activemodel/lib/active_model/type/big_integer.rb
+++ b/activemodel/lib/active_model/type/big_integer.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "integer"
+require "active_model/type/integer"
module ActiveModel
module Type
diff --git a/activemodel/lib/active_model/type/binary.rb b/activemodel/lib/active_model/type/binary.rb
index dc2eca18be..76203c5a88 100644
--- a/activemodel/lib/active_model/type/binary.rb
+++ b/activemodel/lib/active_model/type/binary.rb
@@ -40,7 +40,7 @@ module ActiveModel
alias_method :to_str, :to_s
def hex
- @value.unpack("H*")[0]
+ @value.unpack1("H*")
end
def ==(other)
diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb
index 8cecc16d0f..8ec5deedc4 100644
--- a/activemodel/lib/active_model/type/date.rb
+++ b/activemodel/lib/active_model/type/date.rb
@@ -42,7 +42,7 @@ module ActiveModel
end
def new_date(year, mon, mday)
- if year && year != 0
+ unless year.nil? || (year == 0 && mon == 0 && mday == 0)
::Date.new(year, mon, mday) rescue nil
end
end
diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb
index a4e1427b64..403f0a9e6b 100644
--- a/activemodel/lib/active_model/type/helpers.rb
+++ b/activemodel/lib/active_model/type/helpers.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "helpers/accepts_multiparameter_time"
-require_relative "helpers/numeric"
-require_relative "helpers/mutable"
-require_relative "helpers/time_value"
+require "active_model/type/helpers/accepts_multiparameter_time"
+require "active_model/type/helpers/numeric"
+require "active_model/type/helpers/mutable"
+require "active_model/type/helpers/time_value"
diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb
index fe396998a3..da74aaa3c5 100644
--- a/activemodel/lib/active_model/type/integer.rb
+++ b/activemodel/lib/active_model/type/integer.rb
@@ -31,13 +31,8 @@ module ActiveModel
result
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :range
-
private
+ attr_reader :range
def cast_value(value)
case value
diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb
index 7272d7b0c5..a19dc0f011 100644
--- a/activemodel/lib/active_model/type/registry.rb
+++ b/activemodel/lib/active_model/type/registry.rb
@@ -23,13 +23,8 @@ module ActiveModel
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :registrations
-
private
+ attr_reader :registrations
def registration_klass
Registration
@@ -59,10 +54,7 @@ module ActiveModel
type_name == name
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :name, :block
end
end
diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb
index 6ba2c2a3d2..36f13945b1 100644
--- a/activemodel/lib/active_model/type/string.rb
+++ b/activemodel/lib/active_model/type/string.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "immutable_string"
+require "active_model/type/immutable_string"
module ActiveModel
module Type
diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb
index ad7ba0351a..c094ee0013 100644
--- a/activemodel/lib/active_model/type/time.rb
+++ b/activemodel/lib/active_model/type/time.rb
@@ -28,14 +28,10 @@ module ActiveModel
private
def cast_value(value)
- return value unless value.is_a?(::String)
+ return apply_seconds_precision(value) unless value.is_a?(::String)
return if value.empty?
- if value.start_with?("2000-01-01")
- dummy_time_value = value
- else
- dummy_time_value = "2000-01-01 #{value}"
- end
+ dummy_time_value = value.sub(/\A(\d\d\d\d-\d\d-\d\d |)/, "2000-01-01 ")
fast_string_to_time(dummy_time_value) || begin
time_hash = ::Date._parse(dummy_time_value)
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index cdf11d190f..7f14d102dd 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -164,14 +164,14 @@ module ActiveModel
if options.key?(:on)
options = options.dup
+ options[:on] = Array(options[:on])
options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- !(Array(options[:on]) & Array(o.validation_context)).empty?
+ !(options[:on] & Array(o.validation_context)).empty?
}
end
- args << options
- set_callback(:validate, *args, &block)
+ set_callback(:validate, *args, options, &block)
end
# List all validators that are being used to validate the model using
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index f35e4dec7f..ea3a6b52ab 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -58,13 +58,8 @@ module ActiveModel
klass.send(:attr_writer, *attr_writers)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :attributes
-
private
+ attr_reader :attributes
def convert_to_reader_name(method_name)
method_name.to_s.chomp("=")
diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb
index 4d0ab2a2fe..887d31ae2a 100644
--- a/activemodel/lib/active_model/validations/callbacks.rb
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -54,15 +54,18 @@ module ActiveModel
# person.valid? # => true
# person.name # => "bob"
def before_validation(*args, &block)
- options = args.last
- if options.is_a?(Hash) && options[:on]
- options[:if] = Array(options[:if])
+ options = args.extract_options!
+
+ if options.key?(:on)
+ options = options.dup
options[:on] = Array(options[:on])
+ options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- options[:on].include? o.validation_context
+ !(options[:on] & Array(o.validation_context)).empty?
}
end
- set_callback(:validation, :before, *args, &block)
+
+ set_callback(:validation, :before, *args, options, &block)
end
# Defines a callback that will get called right after validation.
@@ -93,15 +96,18 @@ module ActiveModel
# person.status # => true
def after_validation(*args, &block)
options = args.extract_options!
+ options = options.dup
options[:prepend] = true
- options[:if] = Array(options[:if])
- if options[:on]
+
+ if options.key?(:on)
options[:on] = Array(options[:on])
+ options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- options[:on].include? o.validation_context
+ !(options[:on] & Array(o.validation_context)).empty?
}
end
- set_callback(:validation, :after, *(args << options), &block)
+
+ set_callback(:validation, :after, *args, options, &block)
end
end
diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb
index 0b9b5ce6a1..bafb8e2106 100644
--- a/activemodel/lib/active_model/validations/clusivity.rb
+++ b/activemodel/lib/active_model/validations/clusivity.rb
@@ -32,7 +32,7 @@ module ActiveModel
@delimiter ||= options[:in] || options[:within]
end
- # In Ruby 2.2 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
+ # After Ruby 2.2, <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
# possible values in the range for equality, which is slower but more accurate.
# <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
# endpoints, which is fast but is only accurate on Numeric, Time, Date,
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
index 0abec56b68..1b5d5b09ab 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -9,7 +9,7 @@ module ActiveModel
end
def validate_each(record, attribute, value)
- if (confirmed = record.send("#{attribute}_confirmation"))
+ unless (confirmed = record.send("#{attribute}_confirmation")).nil?
unless confirmation_value_equal?(record, attribute, value, confirmed)
human_attribute_name = record.class.human_attribute_name(attribute)
record.errors.add(:"#{attribute}_confirmation", :confirmation, options.except(:case_sensitive).merge!(attribute: human_attribute_name))
diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb
index a6cbfcc813..3be7ab6ba8 100644
--- a/activemodel/lib/active_model/validations/exclusion.rb
+++ b/activemodel/lib/active_model/validations/exclusion.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "clusivity"
+require "active_model/validations/clusivity"
module ActiveModel
module Validations
diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb
index 00e27b528a..9c12dc14c5 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "clusivity"
+require "active_model/validations/clusivity"
module ActiveModel
module Validations
@@ -19,7 +19,7 @@ module ActiveModel
# particular enumerable object.
#
# class Person < ActiveRecord::Base
- # validates_inclusion_of :gender, in: %w( m f )
+ # validates_inclusion_of :role, in: %w( admin contributor )
# validates_inclusion_of :age, in: 0..99
# validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list"
# validates_inclusion_of :states, in: ->(person) { STATES[person.country] }
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index d1a4197286..d6c80b2c5d 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -31,8 +31,8 @@ module ActiveModel
keys.each do |key|
value = options[key]
- unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY
- raise ArgumentError, ":#{key} must be a nonnegative Integer or Infinity"
+ unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY || value.is_a?(Symbol) || value.is_a?(Proc)
+ raise ArgumentError, ":#{key} must be a nonnegative Integer, Infinity, Symbol, or Proc"
end
end
end
@@ -45,6 +45,12 @@ module ActiveModel
next unless check_value = options[key]
if !value.nil? || skip_nil_check?(key)
+ case check_value
+ when Proc
+ check_value = check_value.call(record)
+ when Symbol
+ check_value = record.send(check_value)
+ end
next if value_length.send(validity_check, check_value)
end
diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb
index 43d9f82d9f..88cca318ef 100644
--- a/activemodel/lib/active_model/validations/validates.rb
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -63,7 +63,7 @@ module ActiveModel
# and strings in shortcut form.
#
# validates :email, format: /@/
- # validates :gender, inclusion: %w(male female)
+ # validates :role, inclusion: %(admin contributor)
# validates :password, length: 6..20
#
# When using shortcut form, ranges and arrays are passed to your
@@ -154,7 +154,7 @@ module ActiveModel
# When creating custom validators, it might be useful to be able to specify
# additional default keys. This can be done by overwriting this method.
def _validates_default_keys
- [:if, :unless, :on, :allow_blank, :allow_nil , :strict]
+ [:if, :unless, :on, :allow_blank, :allow_nil, :strict]
end
def _parse_validates_options(options)
diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb
index 5ecf0a69c4..b06291f1b4 100644
--- a/activemodel/test/cases/attribute_assignment_test.rb
+++ b/activemodel/test/cases/attribute_assignment_test.rb
@@ -18,10 +18,7 @@ class AttributeAssignmentTest < ActiveModel::TestCase
raise ErrorFromAttributeWriter
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_writer :metadata
end
@@ -71,6 +68,14 @@ class AttributeAssignmentTest < ActiveModel::TestCase
assert_equal "world", model.description
end
+ test "simple assignment alias" do
+ model = Model.new
+
+ model.attributes = { name: "hello", description: "world" }
+ assert_equal "hello", model.name
+ assert_equal "world", model.description
+ end
+
test "assign non-existing attribute" do
model = Model.new
error = assert_raises(ActiveModel::UnknownAttributeError) do
diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb
index d2837ec894..0cfc6f4b6b 100644
--- a/activemodel/test/cases/attribute_methods_test.rb
+++ b/activemodel/test/cases/attribute_methods_test.rb
@@ -220,7 +220,7 @@ class AttributeMethodsTest < ActiveModel::TestCase
ModelWithAttributes.define_attribute_methods(:foo)
ModelWithAttributes.undefine_attribute_methods
- assert !ModelWithAttributes.new.respond_to?(:foo)
+ assert_not_respond_to ModelWithAttributes.new, :foo
assert_raises(NoMethodError) { ModelWithAttributes.new.foo }
end
@@ -255,7 +255,7 @@ class AttributeMethodsTest < ActiveModel::TestCase
m = ModelWithAttributes2.new
m.attributes = { "private_method" => "<3", "protected_method" => "O_o" }
- assert !m.respond_to?(:private_method)
+ assert_not_respond_to m, :private_method
assert m.respond_to?(:private_method, true)
c = ClassWithProtected.new
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activemodel/test/cases/attribute_set_test.rb
index 8be77ed88f..b868dba743 100644
--- a/activerecord/test/cases/attribute_set_test.rb
+++ b/activemodel/test/cases/attribute_set_test.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
require "cases/helper"
+require "active_model/attribute_set"
-module ActiveRecord
- class AttributeSetTest < ActiveRecord::TestCase
+module ActiveModel
+ class AttributeSetTest < ActiveModel::TestCase
test "building a new set from raw attributes" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
@@ -73,8 +74,8 @@ module ActiveRecord
clone.freeze
- assert clone.frozen?
- assert_not attributes.frozen?
+ assert_predicate clone, :frozen?
+ assert_not_predicate attributes, :frozen?
end
test "to_hash returns a hash of the type cast values" do
@@ -104,8 +105,8 @@ module ActiveRecord
test "known columns are built with uninitialized attributes" do
attributes = attributes_with_uninitialized_key
- assert attributes[:foo].initialized?
- assert_not attributes[:bar].initialized?
+ assert_predicate attributes[:foo], :initialized?
+ assert_not_predicate attributes[:bar], :initialized?
end
test "uninitialized attributes are not included in the attributes hash" do
@@ -162,12 +163,13 @@ module ActiveRecord
end
test "the primary_key is always initialized" do
- builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, :foo)
+ defaults = { foo: Attribute.from_user(:foo, nil, nil) }
+ builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, defaults)
attributes = builder.build_from_database
assert attributes.key?(:foo)
assert_equal [:foo], attributes.keys
- assert attributes[:foo].initialized?
+ assert_predicate attributes[:foo], :initialized?
end
class MyType
@@ -207,11 +209,6 @@ module ActiveRecord
assert_equal "value from user", attributes.fetch_value(:foo)
end
- def attributes_with_uninitialized_key
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
- builder.build_from_database(foo: "1.1")
- end
-
test "freezing doesn't prevent the set from materializing" do
builder = AttributeSet::Builder.new(foo: Type::String.new)
attributes = builder.build_from_database(foo: "1")
@@ -220,6 +217,22 @@ module ActiveRecord
assert_equal({ foo: "1" }, attributes.to_hash)
end
+ test "marshaling dump/load legacy materialized attribute hash" do
+ builder = AttributeSet::Builder.new(foo: Type::String.new)
+ attributes = builder.build_from_database(foo: "1")
+
+ attributes.instance_variable_get(:@attributes).instance_eval do
+ class << self
+ def marshal_dump
+ materialize
+ end
+ end
+ end
+
+ attributes = Marshal.load(Marshal.dump(attributes))
+ assert_equal({ foo: "1" }, attributes.to_hash)
+ end
+
test "#accessed_attributes returns only attributes which have been read" do
builder = AttributeSet::Builder.new(foo: Type::Value.new, bar: Type::Value.new)
attributes = builder.build_from_database(foo: "1", bar: "2")
@@ -251,5 +264,11 @@ module ActiveRecord
assert_equal attributes, attributes2
assert_not_equal attributes2, attributes3
end
+
+ private
+ def attributes_with_uninitialized_key
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ builder.build_from_database(foo: "1.1")
+ end
end
end
diff --git a/activerecord/test/cases/attribute_test.rb b/activemodel/test/cases/attribute_test.rb
index 1731e7926e..ea2b0efd11 100644
--- a/activerecord/test/cases/attribute_test.rb
+++ b/activemodel/test/cases/attribute_test.rb
@@ -2,8 +2,8 @@
require "cases/helper"
-module ActiveRecord
- class AttributeTest < ActiveRecord::TestCase
+module ActiveModel
+ class AttributeTest < ActiveModel::TestCase
setup do
@type = Minitest::Mock.new
end
@@ -175,33 +175,33 @@ module ActiveRecord
test "an attribute has not been read by default" do
attribute = Attribute.from_database(:foo, 1, Type::Value.new)
- assert_not attribute.has_been_read?
+ assert_not_predicate attribute, :has_been_read?
end
test "an attribute has been read when its value is calculated" do
attribute = Attribute.from_database(:foo, 1, Type::Value.new)
attribute.value
- assert attribute.has_been_read?
+ assert_predicate attribute, :has_been_read?
end
test "an attribute is not changed if it hasn't been assigned or mutated" do
attribute = Attribute.from_database(:foo, 1, Type::Value.new)
- refute attribute.changed?
+ assert_not_predicate attribute, :changed?
end
test "an attribute is changed if it's been assigned a new value" do
attribute = Attribute.from_database(:foo, 1, Type::Value.new)
changed = attribute.with_value_from_user(2)
- assert changed.changed?
+ assert_predicate changed, :changed?
end
test "an attribute is not changed if it's assigned the same value" do
attribute = Attribute.from_database(:foo, 1, Type::Value.new)
unchanged = attribute.with_value_from_user(1)
- refute unchanged.changed?
+ assert_not_predicate unchanged, :changed?
end
test "an attribute can not be mutated if it has not been read,
@@ -209,15 +209,15 @@ module ActiveRecord
type_which_raises_from_all_methods = Object.new
attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods)
- assert_not attribute.changed_in_place?
+ assert_not_predicate attribute, :changed_in_place?
end
test "an attribute is changed if it has been mutated" do
attribute = Attribute.from_database(:foo, "bar", Type::String.new)
attribute.value << "!"
- assert attribute.changed_in_place?
- assert attribute.changed?
+ assert_predicate attribute, :changed_in_place?
+ assert_predicate attribute, :changed?
end
test "an attribute can forget its changes" do
@@ -226,7 +226,7 @@ module ActiveRecord
forgotten = changed.forgetting_assignment
assert changed.changed? # sanity check
- refute forgotten.changed?
+ assert_not_predicate forgotten, :changed?
end
test "with_value_from_user validates the value" do
diff --git a/activemodel/test/cases/attributes_dirty_test.rb b/activemodel/test/cases/attributes_dirty_test.rb
new file mode 100644
index 0000000000..f9693a23cd
--- /dev/null
+++ b/activemodel/test/cases/attributes_dirty_test.rb
@@ -0,0 +1,205 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class AttributesDirtyTest < ActiveModel::TestCase
+ class DirtyModel
+ include ActiveModel::Model
+ include ActiveModel::Attributes
+ include ActiveModel::Dirty
+ attribute :name, :string
+ attribute :color, :string
+ attribute :size, :integer
+
+ def save
+ changes_applied
+ end
+
+ def reload
+ clear_changes_information
+ end
+ end
+
+ setup do
+ @model = DirtyModel.new
+ end
+
+ test "setting attribute will result in change" do
+ assert_not_predicate @model, :changed?
+ assert_not_predicate @model, :name_changed?
+ @model.name = "Ringo"
+ assert_predicate @model, :changed?
+ assert_predicate @model, :name_changed?
+ end
+
+ test "list of changed attribute keys" do
+ assert_equal [], @model.changed
+ @model.name = "Paul"
+ assert_equal ["name"], @model.changed
+ end
+
+ test "changes to attribute values" do
+ assert_not @model.changes["name"]
+ @model.name = "John"
+ assert_equal [nil, "John"], @model.changes["name"]
+ end
+
+ test "checking if an attribute has changed to a particular value" do
+ @model.name = "Ringo"
+ assert @model.name_changed?(from: nil, to: "Ringo")
+ assert_not @model.name_changed?(from: "Pete", to: "Ringo")
+ assert @model.name_changed?(to: "Ringo")
+ assert_not @model.name_changed?(to: "Pete")
+ assert @model.name_changed?(from: nil)
+ assert_not @model.name_changed?(from: "Pete")
+ end
+
+ test "changes accessible through both strings and symbols" do
+ @model.name = "David"
+ assert_not_nil @model.changes[:name]
+ assert_not_nil @model.changes["name"]
+ end
+
+ test "be consistent with symbols arguments after the changes are applied" do
+ @model.name = "David"
+ assert @model.attribute_changed?(:name)
+ @model.save
+ @model.name = "Rafael"
+ assert @model.attribute_changed?(:name)
+ end
+
+ test "attribute mutation" do
+ @model.name = "Yam"
+ @model.save
+ assert_not_predicate @model, :name_changed?
+ @model.name.replace("Hadad")
+ assert_predicate @model, :name_changed?
+ end
+
+ test "resetting attribute" do
+ @model.name = "Bob"
+ @model.restore_name!
+ assert_nil @model.name
+ assert_not_predicate @model, :name_changed?
+ end
+
+ test "setting color to same value should not result in change being recorded" do
+ @model.color = "red"
+ assert_predicate @model, :color_changed?
+ @model.save
+ assert_not_predicate @model, :color_changed?
+ assert_not_predicate @model, :changed?
+ @model.color = "red"
+ assert_not_predicate @model, :color_changed?
+ assert_not_predicate @model, :changed?
+ end
+
+ test "saving should reset model's changed status" do
+ @model.name = "Alf"
+ assert_predicate @model, :changed?
+ @model.save
+ assert_not_predicate @model, :changed?
+ assert_not_predicate @model, :name_changed?
+ end
+
+ test "saving should preserve previous changes" do
+ @model.name = "Jericho Cane"
+ @model.save
+ assert_equal [nil, "Jericho Cane"], @model.previous_changes["name"]
+ end
+
+ test "setting new attributes should not affect previous changes" do
+ @model.name = "Jericho Cane"
+ @model.save
+ @model.name = "DudeFella ManGuy"
+ assert_equal [nil, "Jericho Cane"], @model.name_previous_change
+ end
+
+ test "saving should preserve model's previous changed status" do
+ @model.name = "Jericho Cane"
+ @model.save
+ assert_predicate @model, :name_previously_changed?
+ end
+
+ test "previous value is preserved when changed after save" do
+ assert_equal({}, @model.changed_attributes)
+ @model.name = "Paul"
+ assert_equal({ "name" => nil }, @model.changed_attributes)
+
+ @model.save
+
+ @model.name = "John"
+ assert_equal({ "name" => "Paul" }, @model.changed_attributes)
+ end
+
+ test "changing the same attribute multiple times retains the correct original value" do
+ @model.name = "Otto"
+ @model.save
+ @model.name = "DudeFella ManGuy"
+ @model.name = "Mr. Manfredgensonton"
+ assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change
+ assert_equal @model.name_was, "Otto"
+ end
+
+ test "using attribute_will_change! with a symbol" do
+ @model.size = 1
+ assert_predicate @model, :size_changed?
+ end
+
+ test "reload should reset all changes" do
+ @model.name = "Dmitry"
+ @model.name_changed?
+ @model.save
+ @model.name = "Bob"
+
+ assert_equal [nil, "Dmitry"], @model.previous_changes["name"]
+ assert_equal "Dmitry", @model.changed_attributes["name"]
+
+ @model.reload
+
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes
+ end
+
+ test "restore_attributes should restore all previous data" do
+ @model.name = "Dmitry"
+ @model.color = "Red"
+ @model.save
+ @model.name = "Bob"
+ @model.color = "White"
+
+ @model.restore_attributes
+
+ assert_not_predicate @model, :changed?
+ assert_equal "Dmitry", @model.name
+ assert_equal "Red", @model.color
+ end
+
+ test "restore_attributes can restore only some attributes" do
+ @model.name = "Dmitry"
+ @model.color = "Red"
+ @model.save
+ @model.name = "Bob"
+ @model.color = "White"
+
+ @model.restore_attributes(["name"])
+
+ assert_predicate @model, :changed?
+ assert_equal "Dmitry", @model.name
+ assert_equal "White", @model.color
+ end
+
+ test "changing the attribute reports a change only when the cast value changes" do
+ @model.size = "2.3"
+ @model.save
+ @model.size = "2.1"
+
+ assert_equal false, @model.changed?
+
+ @model.size = "5.1"
+
+ assert_equal true, @model.changed?
+ assert_equal true, @model.size_changed?
+ assert_equal({ "size" => [2, 5] }, @model.changes)
+ end
+end
diff --git a/activemodel/test/cases/attributes_test.rb b/activemodel/test/cases/attributes_test.rb
new file mode 100644
index 0000000000..5483fb101d
--- /dev/null
+++ b/activemodel/test/cases/attributes_test.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ class AttributesTest < ActiveModel::TestCase
+ class ModelForAttributesTest
+ include ActiveModel::Model
+ include ActiveModel::Attributes
+
+ attribute :integer_field, :integer
+ attribute :string_field, :string
+ attribute :decimal_field, :decimal
+ attribute :string_with_default, :string, default: "default string"
+ attribute :date_field, :date, default: -> { Date.new(2016, 1, 1) }
+ attribute :boolean_field, :boolean
+ end
+
+ class ChildModelForAttributesTest < ModelForAttributesTest
+ end
+
+ class GrandchildModelForAttributesTest < ChildModelForAttributesTest
+ attribute :integer_field, :string
+ end
+
+ test "properties assignment" do
+ data = ModelForAttributesTest.new(
+ integer_field: "2.3",
+ string_field: "Rails FTW",
+ decimal_field: "12.3",
+ boolean_field: "0"
+ )
+
+ assert_equal 2, data.integer_field
+ assert_equal "Rails FTW", data.string_field
+ assert_equal BigDecimal("12.3"), data.decimal_field
+ assert_equal "default string", data.string_with_default
+ assert_equal Date.new(2016, 1, 1), data.date_field
+ assert_equal false, data.boolean_field
+
+ data.integer_field = 10
+ data.string_with_default = nil
+ data.boolean_field = "1"
+
+ assert_equal 10, data.integer_field
+ assert_nil data.string_with_default
+ assert_equal true, data.boolean_field
+ end
+
+ test "reading attributes" do
+ data = ModelForAttributesTest.new(
+ integer_field: 1.1,
+ string_field: 1.1,
+ decimal_field: 1.1,
+ boolean_field: 1.1
+ )
+
+ expected_attributes = {
+ integer_field: 1,
+ string_field: "1.1",
+ decimal_field: BigDecimal("1.1"),
+ string_with_default: "default string",
+ date_field: Date.new(2016, 1, 1),
+ boolean_field: true
+ }.stringify_keys
+
+ assert_equal expected_attributes, data.attributes
+ end
+
+ test "nonexistent attribute" do
+ assert_raise ActiveModel::UnknownAttributeError do
+ ModelForAttributesTest.new(nonexistent: "nonexistent")
+ end
+ end
+
+ test "children inherit attributes" do
+ data = ChildModelForAttributesTest.new(integer_field: "4.4")
+
+ assert_equal 4, data.integer_field
+ end
+
+ test "children can override parents" do
+ data = GrandchildModelForAttributesTest.new(integer_field: "4.4")
+
+ assert_equal "4.4", data.integer_field
+ end
+
+ test "attributes with proc defaults can be marshalled" do
+ data = ModelForAttributesTest.new
+ attributes = data.instance_variable_get(:@attributes)
+ round_tripped = Marshal.load(Marshal.dump(data))
+ new_attributes = round_tripped.instance_variable_get(:@attributes)
+
+ assert_equal attributes, new_attributes
+ end
+ end
+end
diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb
index a5d29d0f22..1ec12d8222 100644
--- a/activemodel/test/cases/callbacks_test.rb
+++ b/activemodel/test/cases/callbacks_test.rb
@@ -85,21 +85,21 @@ class CallbacksTest < ActiveModel::TestCase
end
test "only selects which types of callbacks should be created" do
- assert !ModelCallbacks.respond_to?(:before_initialize)
- assert !ModelCallbacks.respond_to?(:around_initialize)
+ assert_not_respond_to ModelCallbacks, :before_initialize
+ assert_not_respond_to ModelCallbacks, :around_initialize
assert_respond_to ModelCallbacks, :after_initialize
end
test "only selects which types of callbacks should be created from an array list" do
assert_respond_to ModelCallbacks, :before_multiple
assert_respond_to ModelCallbacks, :around_multiple
- assert !ModelCallbacks.respond_to?(:after_multiple)
+ assert_not_respond_to ModelCallbacks, :after_multiple
end
test "no callbacks should be created" do
- assert !ModelCallbacks.respond_to?(:before_empty)
- assert !ModelCallbacks.respond_to?(:around_empty)
- assert !ModelCallbacks.respond_to?(:after_empty)
+ assert_not_respond_to ModelCallbacks, :before_empty
+ assert_not_respond_to ModelCallbacks, :around_empty
+ assert_not_respond_to ModelCallbacks, :after_empty
end
class Violin
diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb
index 2cd9e185e6..b120e68027 100644
--- a/activemodel/test/cases/dirty_test.rb
+++ b/activemodel/test/cases/dirty_test.rb
@@ -5,12 +5,13 @@ require "cases/helper"
class DirtyTest < ActiveModel::TestCase
class DirtyModel
include ActiveModel::Dirty
- define_attribute_methods :name, :color, :size
+ define_attribute_methods :name, :color, :size, :status
def initialize
@name = nil
@color = nil
@size = nil
+ @status = "initialized"
end
def name
@@ -40,6 +41,15 @@ class DirtyTest < ActiveModel::TestCase
@size = val
end
+ def status
+ @status
+ end
+
+ def status=(val)
+ status_will_change! unless val == @status
+ @status = val
+ end
+
def save
changes_applied
end
@@ -54,11 +64,11 @@ class DirtyTest < ActiveModel::TestCase
end
test "setting attribute will result in change" do
- assert !@model.changed?
- assert !@model.name_changed?
+ assert_not_predicate @model, :changed?
+ assert_not_predicate @model, :name_changed?
@model.name = "Ringo"
- assert @model.changed?
- assert @model.name_changed?
+ assert_predicate @model, :changed?
+ assert_predicate @model, :name_changed?
end
test "list of changed attribute keys" do
@@ -68,7 +78,7 @@ class DirtyTest < ActiveModel::TestCase
end
test "changes to attribute values" do
- assert !@model.changes["name"]
+ assert_not @model.changes["name"]
@model.name = "John"
assert_equal [nil, "John"], @model.changes["name"]
end
@@ -99,82 +109,93 @@ class DirtyTest < ActiveModel::TestCase
test "attribute mutation" do
@model.instance_variable_set("@name", "Yam".dup)
- assert !@model.name_changed?
+ assert_not_predicate @model, :name_changed?
@model.name.replace("Hadad")
- assert !@model.name_changed?
+ assert_not_predicate @model, :name_changed?
@model.name_will_change!
@model.name.replace("Baal")
- assert @model.name_changed?
+ assert_predicate @model, :name_changed?
end
test "resetting attribute" do
@model.name = "Bob"
@model.restore_name!
assert_nil @model.name
- assert !@model.name_changed?
+ assert_not_predicate @model, :name_changed?
end
test "setting color to same value should not result in change being recorded" do
@model.color = "red"
- assert @model.color_changed?
+ assert_predicate @model, :color_changed?
@model.save
- assert !@model.color_changed?
- assert !@model.changed?
+ assert_not_predicate @model, :color_changed?
+ assert_not_predicate @model, :changed?
@model.color = "red"
- assert !@model.color_changed?
- assert !@model.changed?
+ assert_not_predicate @model, :color_changed?
+ assert_not_predicate @model, :changed?
end
test "saving should reset model's changed status" do
@model.name = "Alf"
- assert @model.changed?
+ assert_predicate @model, :changed?
@model.save
- assert !@model.changed?
- assert !@model.name_changed?
+ assert_not_predicate @model, :changed?
+ assert_not_predicate @model, :name_changed?
end
test "saving should preserve previous changes" do
@model.name = "Jericho Cane"
+ @model.status = "waiting"
@model.save
assert_equal [nil, "Jericho Cane"], @model.previous_changes["name"]
+ assert_equal ["initialized", "waiting"], @model.previous_changes["status"]
end
test "setting new attributes should not affect previous changes" do
@model.name = "Jericho Cane"
+ @model.status = "waiting"
@model.save
@model.name = "DudeFella ManGuy"
+ @model.status = "finished"
assert_equal [nil, "Jericho Cane"], @model.name_previous_change
+ assert_equal ["initialized", "waiting"], @model.previous_changes["status"]
end
test "saving should preserve model's previous changed status" do
@model.name = "Jericho Cane"
@model.save
- assert @model.name_previously_changed?
+ assert_predicate @model, :name_previously_changed?
end
test "previous value is preserved when changed after save" do
assert_equal({}, @model.changed_attributes)
@model.name = "Paul"
- assert_equal({ "name" => nil }, @model.changed_attributes)
+ @model.status = "waiting"
+ assert_equal({ "name" => nil, "status" => "initialized" }, @model.changed_attributes)
@model.save
@model.name = "John"
- assert_equal({ "name" => "Paul" }, @model.changed_attributes)
+ @model.status = "finished"
+ assert_equal({ "name" => "Paul", "status" => "waiting" }, @model.changed_attributes)
end
test "changing the same attribute multiple times retains the correct original value" do
@model.name = "Otto"
+ @model.status = "waiting"
@model.save
@model.name = "DudeFella ManGuy"
@model.name = "Mr. Manfredgensonton"
+ @model.status = "processing"
+ @model.status = "finished"
assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change
+ assert_equal ["waiting", "finished"], @model.status_change
assert_equal @model.name_was, "Otto"
end
test "using attribute_will_change! with a symbol" do
@model.size = 1
- assert @model.size_changed?
+ assert_predicate @model, :size_changed?
end
test "reload should reset all changes" do
@@ -201,7 +222,7 @@ class DirtyTest < ActiveModel::TestCase
@model.restore_attributes
- assert_not @model.changed?
+ assert_not_predicate @model, :changed?
assert_equal "Dmitry", @model.name
assert_equal "Red", @model.color
end
@@ -215,8 +236,12 @@ class DirtyTest < ActiveModel::TestCase
@model.restore_attributes(["name"])
- assert @model.changed?
+ assert_predicate @model, :changed?
assert_equal "Dmitry", @model.name
assert_equal "White", @model.color
end
+
+ test "model can be dup-ed without Attributes" do
+ assert @model.dup
+ end
end
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index ab18af0de1..6ff3be1308 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_support/core_ext/string/strip"
require "yaml"
class ErrorsTest < ActiveModel::TestCase
@@ -83,7 +82,7 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal 1, person.errors.count
person.errors.clear
- assert person.errors.empty?
+ assert_empty person.errors
end
test "error access is indifferent" do
@@ -128,8 +127,8 @@ class ErrorsTest < ActiveModel::TestCase
test "detecting whether there are errors with empty?, blank?, include?" do
person = Person.new
person.errors[:foo]
- assert person.errors.empty?
- assert person.errors.blank?
+ assert_empty person.errors
+ assert_predicate person.errors, :blank?
assert_not_includes person.errors, :foo
end
@@ -208,19 +207,26 @@ class ErrorsTest < ActiveModel::TestCase
test "added? returns false when no errors are present" do
person = Person.new
- assert !person.errors.added?(:name)
+ assert_not person.errors.added?(:name)
end
test "added? returns false when checking a nonexisting error and other errors are present for the given attribute" do
person = Person.new
person.errors.add(:name, "is invalid")
- assert !person.errors.added?(:name, "cannot be blank")
+ assert_not person.errors.added?(:name, "cannot be blank")
end
test "added? returns false when checking for an error, but not providing message arguments" do
person = Person.new
person.errors.add(:name, "cannot be blank")
- assert !person.errors.added?(:name)
+ assert_not person.errors.added?(:name)
+ end
+
+ test "added? returns false when checking for an error by symbol and a different error with same message is present" do
+ I18n.backend.store_translations("en", errors: { attributes: { name: { wrong: "is wrong", used: "is wrong" } } })
+ person = Person.new
+ person.errors.add(:name, :wrong)
+ assert_not person.errors.added?(:name, :used)
end
test "size calculates the number of error messages" do
@@ -313,7 +319,7 @@ class ErrorsTest < ActiveModel::TestCase
test "generate_message works without i18n_scope" do
person = Person.new
- assert !Person.respond_to?(:i18n_scope)
+ assert_not_respond_to Person, :i18n_scope
assert_nothing_raised {
person.errors.generate_message(:name, :blank)
}
@@ -364,7 +370,7 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal 1, person.errors.details.count
person.errors.clear
- assert person.errors.details.empty?
+ assert_empty person.errors.details
end
test "copy errors" do
@@ -399,7 +405,7 @@ class ErrorsTest < ActiveModel::TestCase
end
test "errors are backward compatible with the Rails 4.2 format" do
- yaml = <<-CODE.strip_heredoc
+ yaml = <<~CODE
--- !ruby/object:ActiveModel::Errors
base: &1 !ruby/object:ErrorsTest::Person
errors: !ruby/object:ActiveModel::Errors
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index d19e81a119..912cb6d5ab 100644
--- a/activemodel/test/cases/secure_password_test.rb
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -187,7 +187,7 @@ class SecurePasswordTest < ActiveModel::TestCase
test "authenticate" do
@user.password = "secret"
- assert !@user.authenticate("wrong")
+ assert_not @user.authenticate("wrong")
assert @user.authenticate("secret")
end
diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb
index 9002982e7f..6826d2bbd1 100644
--- a/activemodel/test/cases/serialization_test.rb
+++ b/activemodel/test/cases/serialization_test.rb
@@ -174,4 +174,11 @@ class SerializationTest < ActiveModel::TestCase
{ "name" => "Sue", "email" => "sue@example.com", "gender" => "female" }] }
assert_equal expected, @user.serializable_hash(include: [{ address: { only: "street" } }, :friends])
end
+
+ def test_all_includes_with_options
+ expected = { "email" => "david@example.com", "gender" => "male", "name" => "David",
+ "address" => { "street" => "123 Lane" },
+ "friends" => [{ "name" => "Joe" }, { "name" => "Sue" }] }
+ assert_equal expected, @user.serializable_hash(include: [address: { only: "street" }, friends: { only: "name" }])
+ end
end
diff --git a/activemodel/test/cases/type/big_integer_test.rb b/activemodel/test/cases/type/big_integer_test.rb
index 3d29235d52..0fa0200df4 100644
--- a/activemodel/test/cases/type/big_integer_test.rb
+++ b/activemodel/test/cases/type/big_integer_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/binary_test.rb b/activemodel/test/cases/type/binary_test.rb
index ef4f125a3b..3221a73e49 100644
--- a/activemodel/test/cases/type/binary_test.rb
+++ b/activemodel/test/cases/type/binary_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/boolean_test.rb b/activemodel/test/cases/type/boolean_test.rb
index 97b165ab48..2de0f53640 100644
--- a/activemodel/test/cases/type/boolean_test.rb
+++ b/activemodel/test/cases/type/boolean_test.rb
@@ -1,15 +1,14 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
class BooleanTest < ActiveModel::TestCase
def test_type_cast_boolean
type = Type::Boolean.new
- assert type.cast("").nil?
- assert type.cast(nil).nil?
+ assert_predicate type.cast(""), :nil?
+ assert_predicate type.cast(nil), :nil?
assert type.cast(true)
assert type.cast(1)
diff --git a/activemodel/test/cases/type/date_test.rb b/activemodel/test/cases/type/date_test.rb
index 15c40a37b7..e8cf178612 100644
--- a/activemodel/test/cases/type/date_test.rb
+++ b/activemodel/test/cases/type/date_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/date_time_test.rb b/activemodel/test/cases/type/date_time_test.rb
index 598ccf485e..60f62becc2 100644
--- a/activemodel/test/cases/type/date_time_test.rb
+++ b/activemodel/test/cases/type/date_time_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb
index a0acdc2736..c0cf6ce590 100644
--- a/activemodel/test/cases/type/decimal_test.rb
+++ b/activemodel/test/cases/type/decimal_test.rb
@@ -1,29 +1,28 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
class DecimalTest < ActiveModel::TestCase
def test_type_cast_decimal
type = Decimal.new
- assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0"))
- assert_equal BigDecimal.new("123"), type.cast(123.0)
- assert_equal BigDecimal.new("1"), type.cast(:"1")
+ assert_equal BigDecimal("0"), type.cast(BigDecimal("0"))
+ assert_equal BigDecimal("123"), type.cast(123.0)
+ assert_equal BigDecimal("1"), type.cast(:"1")
end
def test_type_cast_decimal_from_invalid_string
type = Decimal.new
assert_nil type.cast("")
- assert_equal BigDecimal.new("1"), type.cast("1ignore")
- assert_equal BigDecimal.new("0"), type.cast("bad1")
- assert_equal BigDecimal.new("0"), type.cast("bad")
+ assert_equal BigDecimal("1"), type.cast("1ignore")
+ assert_equal BigDecimal("0"), type.cast("bad1")
+ assert_equal BigDecimal("0"), type.cast("bad")
end
def test_type_cast_decimal_from_float_with_large_precision
type = Decimal.new(precision: ::Float::DIG + 2)
- assert_equal BigDecimal.new("123.0"), type.cast(123.0)
+ assert_equal BigDecimal("123.0"), type.cast(123.0)
end
def test_type_cast_from_float_with_unspecified_precision
@@ -49,7 +48,7 @@ module ActiveModel
def test_type_cast_decimal_from_object_responding_to_d
value = Object.new
def value.to_d
- BigDecimal.new("1")
+ BigDecimal("1")
end
type = Decimal.new
assert_equal BigDecimal("1"), type.cast(value)
diff --git a/activemodel/test/cases/type/float_test.rb b/activemodel/test/cases/type/float_test.rb
index 46e8b34dfe..28318e06f8 100644
--- a/activemodel/test/cases/type/float_test.rb
+++ b/activemodel/test/cases/type/float_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/immutable_string_test.rb b/activemodel/test/cases/type/immutable_string_test.rb
index 72f5779dfb..751f753ddb 100644
--- a/activemodel/test/cases/type/immutable_string_test.rb
+++ b/activemodel/test/cases/type/immutable_string_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb
index d2e635b447..8c5d18c9b3 100644
--- a/activemodel/test/cases/type/integer_test.rb
+++ b/activemodel/test/cases/type/integer_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_model/type"
require "active_support/core_ext/numeric/time"
module ActiveModel
diff --git a/activemodel/test/cases/type/registry_test.rb b/activemodel/test/cases/type/registry_test.rb
index f34104286c..0633ea2538 100644
--- a/activemodel/test/cases/type/registry_test.rb
+++ b/activemodel/test/cases/type/registry_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb
index d39389718b..825c8bb246 100644
--- a/activemodel/test/cases/type/string_test.rb
+++ b/activemodel/test/cases/type/string_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/time_test.rb b/activemodel/test/cases/type/time_test.rb
index 0bea95768d..f7102d1e97 100644
--- a/activemodel/test/cases/type/time_test.rb
+++ b/activemodel/test/cases/type/time_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/value_test.rb b/activemodel/test/cases/type/value_test.rb
index 671343b0c8..55b5d9d584 100644
--- a/activemodel/test/cases/type/value_test.rb
+++ b/activemodel/test/cases/type/value_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/validations/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb
index 801577474a..8bc4f4723a 100644
--- a/activemodel/test/cases/validations/absence_validation_test.rb
+++ b/activemodel/test/cases/validations/absence_validation_test.rb
@@ -17,16 +17,16 @@ class AbsenceValidationTest < ActiveModel::TestCase
t = Topic.new
t.title = "foo"
t.content = "bar"
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["must be blank"], t.errors[:title]
assert_equal ["must be blank"], t.errors[:content]
t.title = ""
t.content = "something"
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["must be blank"], t.errors[:content]
assert_equal [], t.errors[:title]
t.content = ""
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_absence_of_with_array_arguments
@@ -34,7 +34,7 @@ class AbsenceValidationTest < ActiveModel::TestCase
t = Topic.new
t.title = "foo"
t.content = "bar"
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["must be blank"], t.errors[:title]
assert_equal ["must be blank"], t.errors[:content]
end
@@ -43,7 +43,7 @@ class AbsenceValidationTest < ActiveModel::TestCase
Person.validates_absence_of :karma, message: "This string contains 'single' and \"double\" quotes"
p = Person.new
p.karma = "good"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last
end
@@ -51,19 +51,19 @@ class AbsenceValidationTest < ActiveModel::TestCase
Person.validates_absence_of :karma
p = Person.new
p.karma = "good"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["must be blank"], p.errors[:karma]
p.karma = nil
- assert p.valid?
+ assert_predicate p, :valid?
end
def test_validates_absence_of_for_ruby_class_with_custom_reader
CustomReader.validates_absence_of :karma
p = CustomReader.new
p[:karma] = "excellent"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["must be blank"], p.errors[:karma]
p[:karma] = ""
- assert p.valid?
+ assert_predicate p, :valid?
end
end
diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb
index c5f54b1868..7662f996ae 100644
--- a/activemodel/test/cases/validations/acceptance_validation_test.rb
+++ b/activemodel/test/cases/validations/acceptance_validation_test.rb
@@ -15,54 +15,54 @@ class AcceptanceValidationTest < ActiveModel::TestCase
Topic.validates_acceptance_of(:terms_of_service)
t = Topic.new("title" => "We should not be confirmed")
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_terms_of_service_agreement
Topic.validates_acceptance_of(:terms_of_service)
t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["must be accepted"], t.errors[:terms_of_service]
t.terms_of_service = "1"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_eula
Topic.validates_acceptance_of(:eula, message: "must be abided")
t = Topic.new("title" => "We should be confirmed", "eula" => "")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["must be abided"], t.errors[:eula]
t.eula = "1"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_terms_of_service_agreement_with_accept_value
Topic.validates_acceptance_of(:terms_of_service, accept: "I agree.")
t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["must be accepted"], t.errors[:terms_of_service]
t.terms_of_service = "I agree."
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_terms_of_service_agreement_with_multiple_accept_values
Topic.validates_acceptance_of(:terms_of_service, accept: [1, "I concur."])
t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["must be accepted"], t.errors[:terms_of_service]
t.terms_of_service = 1
- assert t.valid?
+ assert_predicate t, :valid?
t.terms_of_service = "I concur."
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_acceptance_of_for_ruby_class
@@ -71,11 +71,11 @@ class AcceptanceValidationTest < ActiveModel::TestCase
p = Person.new
p.karma = ""
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["must be accepted"], p.errors[:karma]
p.karma = "1"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
@@ -83,6 +83,6 @@ class AcceptanceValidationTest < ActiveModel::TestCase
def test_validates_acceptance_of_true
Topic.validates_acceptance_of(:terms_of_service)
- assert Topic.new(terms_of_service: true).valid?
+ assert_predicate Topic.new(terms_of_service: true), :valid?
end
end
diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb
index d3a9a17a05..ff3cf61746 100644
--- a/activemodel/test/cases/validations/callbacks_test.rb
+++ b/activemodel/test/cases/validations/callbacks_test.rb
@@ -59,6 +59,18 @@ class DogValidatorWithOnCondition < Dog
def set_after_validation_marker; history << "after_validation_marker" ; end
end
+class DogValidatorWithOnMultipleCondition < Dog
+ before_validation :set_before_validation_marker_on_context_a, on: :context_a
+ before_validation :set_before_validation_marker_on_context_b, on: :context_b
+ after_validation :set_after_validation_marker_on_context_a, on: :context_a
+ after_validation :set_after_validation_marker_on_context_b, on: :context_b
+
+ def set_before_validation_marker_on_context_a; history << "before_validation_marker on context_a"; end
+ def set_before_validation_marker_on_context_b; history << "before_validation_marker on context_b"; end
+ def set_after_validation_marker_on_context_a; history << "after_validation_marker on context_a" ; end
+ def set_after_validation_marker_on_context_b; history << "after_validation_marker on context_b" ; end
+end
+
class DogValidatorWithIfCondition < Dog
before_validation :set_before_validation_marker1, if: -> { true }
before_validation :set_before_validation_marker2, if: -> { false }
@@ -98,6 +110,37 @@ class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase
assert_equal [], d.history
end
+ def test_on_multiple_condition_is_respected_for_validation_with_matching_context
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?(:context_a)
+ assert_equal ["before_validation_marker on context_a", "after_validation_marker on context_a"], d.history
+
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?(:context_b)
+ assert_equal ["before_validation_marker on context_b", "after_validation_marker on context_b"], d.history
+
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?([:context_a, :context_b])
+ assert_equal([
+ "before_validation_marker on context_a",
+ "before_validation_marker on context_b",
+ "after_validation_marker on context_a",
+ "after_validation_marker on context_b"
+ ], d.history)
+ end
+
+ def test_on_multiple_condition_is_respected_for_validation_without_matching_context
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?(:save)
+ assert_equal [], d.history
+ end
+
+ def test_on_multiple_condition_is_respected_for_validation_without_context
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?
+ assert_equal [], d.history
+ end
+
def test_before_validation_and_after_validation_callbacks_should_be_called
d = DogWithMethodCallbacks.new
d.valid?
diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb
index 68dade556c..1704db9a48 100644
--- a/activemodel/test/cases/validations/conditional_validation_test.rb
+++ b/activemodel/test/cases/validations/conditional_validation_test.rb
@@ -13,75 +13,63 @@ class ConditionalValidationTest < ActiveModel::TestCase
# When the method returns true
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors["title"]
end
- def test_unless_validation_using_method_true
- # When the method returns true
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true)
+ def test_if_validation_using_array_of_true_methods
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: [:condition_is_true, :condition_is_true])
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.valid?
- assert_empty t.errors[:title]
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
+ assert_equal ["hoo 5"], t.errors["title"]
end
- def test_if_validation_using_method_false
- # When the method returns false
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true_but_its_not)
+ def test_unless_validation_using_array_of_false_methods
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: [:condition_is_false, :condition_is_false])
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.valid?
- assert_empty t.errors[:title]
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
+ assert_equal ["hoo 5"], t.errors["title"]
end
- def test_unless_validation_using_method_false
- # When the method returns false
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true_but_its_not)
+ def test_unless_validation_using_method_true
+ # When the method returns true
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
- assert_equal ["hoo 5"], t.errors["title"]
+ assert_predicate t, :valid?
+ assert_empty t.errors[:title]
end
- def test_if_validation_using_string_true
- # When the evaluated string returns true
- ActiveSupport::Deprecation.silence do
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "a = 1; a == 1")
- end
+ def test_if_validation_using_array_of_true_and_false_methods
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: [:condition_is_true, :condition_is_false])
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
- assert_equal ["hoo 5"], t.errors["title"]
+ assert_predicate t, :valid?
+ assert_empty t.errors[:title]
end
- def test_unless_validation_using_string_true
- # When the evaluated string returns true
- ActiveSupport::Deprecation.silence do
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1")
- end
+ def test_unless_validation_using_array_of_true_and_felse_methods
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: [:condition_is_true, :condition_is_false])
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
assert_empty t.errors[:title]
end
- def test_if_validation_using_string_false
- # When the evaluated string returns false
- ActiveSupport::Deprecation.silence do
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false")
- end
+ def test_if_validation_using_method_false
+ # When the method returns false
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_false)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
assert_empty t.errors[:title]
end
- def test_unless_validation_using_string_false
- # When the evaluated string returns false
- ActiveSupport::Deprecation.silence do
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "false")
- end
+ def test_unless_validation_using_method_false
+ # When the method returns false
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_false)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors["title"]
end
@@ -90,8 +78,8 @@ class ConditionalValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
if: Proc.new { |r| r.content.size > 4 })
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors["title"]
end
@@ -100,7 +88,7 @@ class ConditionalValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
unless: Proc.new { |r| r.content.size > 4 })
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
assert_empty t.errors[:title]
end
@@ -109,7 +97,7 @@ class ConditionalValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
if: Proc.new { |r| r.title != "uhohuhoh" })
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
assert_empty t.errors[:title]
end
@@ -118,32 +106,23 @@ class ConditionalValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
unless: Proc.new { |r| r.title != "uhohuhoh" })
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors["title"]
end
- # previous implementation of validates_presence_of eval'd the
- # string with the wrong binding, this regression test is to
- # ensure that it works correctly
- def test_validation_with_if_as_string
- Topic.validates_presence_of(:title)
- ActiveSupport::Deprecation.silence do
- Topic.validates_presence_of(:author_name, if: "title.to_s.match('important')")
- end
-
- t = Topic.new
- assert t.invalid?, "A topic without a title should not be valid"
- assert_empty t.errors[:author_name], "A topic without an 'important' title should not require an author"
-
- t.title = "Just a title"
- assert t.valid?, "A topic with a basic title should be valid"
-
- t.title = "A very important title"
- assert t.invalid?, "A topic with an important title, but without an author, should not be valid"
- assert t.errors[:author_name].any?, "A topic with an 'important' title should require an author"
+ def test_validation_using_conbining_if_true_and_unless_true_conditions
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true, unless: :condition_is_true)
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert_predicate t, :valid?
+ assert_empty t.errors[:title]
+ end
- t.author_name = "Hubert J. Farnsworth"
- assert t.valid?, "A topic with an important title and author should be valid"
+ def test_validation_using_conbining_if_true_and_unless_false_conditions
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true, unless: :condition_is_false)
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
+ assert_equal ["hoo 5"], t.errors["title"]
end
end
diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb
index e84415a868..8603a8ac5c 100644
--- a/activemodel/test/cases/validations/confirmation_validation_test.rb
+++ b/activemodel/test/cases/validations/confirmation_validation_test.rb
@@ -14,27 +14,40 @@ class ConfirmationValidationTest < ActiveModel::TestCase
Topic.validates_confirmation_of(:title)
t = Topic.new(author_name: "Plutarch")
- assert t.valid?
+ assert_predicate t, :valid?
t.title_confirmation = "Parallel Lives"
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title_confirmation = nil
t.title = "Parallel Lives"
- assert t.valid?
+ assert_predicate t, :valid?
t.title_confirmation = "Parallel Lives"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_title_confirmation
Topic.validates_confirmation_of(:title)
t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "")
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title_confirmation = "We should be confirmed"
- assert t.valid?
+ assert_predicate t, :valid?
+ end
+
+ def test_validates_confirmation_of_with_boolean_attribute
+ Topic.validates_confirmation_of(:approved)
+
+ t = Topic.new(approved: true, approved_confirmation: nil)
+ assert_predicate t, :valid?
+
+ t.approved_confirmation = false
+ assert_predicate t, :invalid?
+
+ t.approved_confirmation = true
+ assert_predicate t, :valid?
end
def test_validates_confirmation_of_for_ruby_class
@@ -42,12 +55,12 @@ class ConfirmationValidationTest < ActiveModel::TestCase
p = Person.new
p.karma_confirmation = "None"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["doesn't match Karma"], p.errors[:karma_confirmation]
p.karma = "None"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
@@ -64,7 +77,7 @@ class ConfirmationValidationTest < ActiveModel::TestCase
Topic.validates_confirmation_of(:title)
t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation]
ensure
I18n.load_path.replace @old_load_path
@@ -109,13 +122,13 @@ class ConfirmationValidationTest < ActiveModel::TestCase
Topic.validates_confirmation_of(:title, case_sensitive: true)
t = Topic.new(title: "title", title_confirmation: "Title")
- assert t.invalid?
+ assert_predicate t, :invalid?
end
def test_title_confirmation_with_case_sensitive_option_false
Topic.validates_confirmation_of(:title, case_sensitive: false)
t = Topic.new(title: "title", title_confirmation: "Title")
- assert t.valid?
+ assert_predicate t, :valid?
end
end
diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb
index 68d611e904..50bd47065c 100644
--- a/activemodel/test/cases/validations/exclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/exclusion_validation_test.rb
@@ -14,8 +14,8 @@ class ExclusionValidationTest < ActiveModel::TestCase
def test_validates_exclusion_of
Topic.validates_exclusion_of(:title, in: %w( abe monkey ))
- assert Topic.new("title" => "something", "content" => "abc").valid?
- assert Topic.new("title" => "monkey", "content" => "abc").invalid?
+ assert_predicate Topic.new("title" => "something", "content" => "abc"), :valid?
+ assert_predicate Topic.new("title" => "monkey", "content" => "abc"), :invalid?
end
def test_validates_exclusion_of_with_formatted_message
@@ -24,8 +24,8 @@ class ExclusionValidationTest < ActiveModel::TestCase
assert Topic.new("title" => "something", "content" => "abc")
t = Topic.new("title" => "monkey")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["option monkey is restricted"], t.errors[:title]
end
@@ -35,8 +35,8 @@ class ExclusionValidationTest < ActiveModel::TestCase
assert Topic.new("title" => "something", "content" => "abc")
t = Topic.new("title" => "monkey")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
end
def test_validates_exclusion_of_for_ruby_class
@@ -44,12 +44,12 @@ class ExclusionValidationTest < ActiveModel::TestCase
p = Person.new
p.karma = "abe"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is reserved"], p.errors[:karma]
p.karma = "Lifo"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
@@ -60,26 +60,26 @@ class ExclusionValidationTest < ActiveModel::TestCase
t = Topic.new
t.title = "elephant"
t.author_name = "sikachu"
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title = "wasabi"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_exclusion_of_with_range
Topic.validates_exclusion_of :content, in: ("a".."g")
- assert Topic.new(content: "g").invalid?
- assert Topic.new(content: "h").valid?
+ assert_predicate Topic.new(content: "g"), :invalid?
+ assert_predicate Topic.new(content: "h"), :valid?
end
def test_validates_exclusion_of_with_time_range
Topic.validates_exclusion_of :created_at, in: 6.days.ago..2.days.ago
- assert Topic.new(created_at: 5.days.ago).invalid?
- assert Topic.new(created_at: 3.days.ago).invalid?
- assert Topic.new(created_at: 7.days.ago).valid?
- assert Topic.new(created_at: 1.day.ago).valid?
+ assert_predicate Topic.new(created_at: 5.days.ago), :invalid?
+ assert_predicate Topic.new(created_at: 3.days.ago), :invalid?
+ assert_predicate Topic.new(created_at: 7.days.ago), :valid?
+ assert_predicate Topic.new(created_at: 1.day.ago), :valid?
end
def test_validates_inclusion_of_with_symbol
@@ -92,7 +92,7 @@ class ExclusionValidationTest < ActiveModel::TestCase
%w(abe)
end
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is reserved"], p.errors[:karma]
p = Person.new
@@ -102,7 +102,7 @@ class ExclusionValidationTest < ActiveModel::TestCase
%w()
end
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb
index 3ddda2154a..2a7088b3e8 100644
--- a/activemodel/test/cases/validations/format_validation_test.rb
+++ b/activemodel/test/cases/validations/format_validation_test.rb
@@ -5,7 +5,7 @@ require "cases/helper"
require "models/topic"
require "models/person"
-class PresenceValidationTest < ActiveModel::TestCase
+class FormatValidationTest < ActiveModel::TestCase
def teardown
Topic.clear_validators!
end
@@ -16,22 +16,22 @@ class PresenceValidationTest < ActiveModel::TestCase
t = Topic.new("title" => "i'm incorrect", "content" => "Validation macros rule!")
assert t.invalid?, "Shouldn't be valid"
assert_equal ["is bad data"], t.errors[:title]
- assert t.errors[:content].empty?
+ assert_empty t.errors[:content]
t.title = "Validation macros rule!"
- assert t.valid?
- assert t.errors[:title].empty?
+ assert_predicate t, :valid?
+ assert_empty t.errors[:title]
assert_raise(ArgumentError) { Topic.validates_format_of(:title, :content) }
end
def test_validate_format_with_allow_blank
Topic.validates_format_of(:title, with: /\AValidation\smacros \w+!\z/, allow_blank: true)
- assert Topic.new("title" => "Shouldn't be valid").invalid?
- assert Topic.new("title" => "").valid?
- assert Topic.new("title" => nil).valid?
- assert Topic.new("title" => "Validation macros rule!").valid?
+ assert_predicate Topic.new("title" => "Shouldn't be valid"), :invalid?
+ assert_predicate Topic.new("title" => ""), :valid?
+ assert_predicate Topic.new("title" => nil), :valid?
+ assert_predicate Topic.new("title" => "Validation macros rule!"), :valid?
end
# testing ticket #3142
@@ -42,7 +42,7 @@ class PresenceValidationTest < ActiveModel::TestCase
assert t.invalid?, "Shouldn't be valid"
assert_equal ["is bad data"], t.errors[:title]
- assert t.errors[:content].empty?
+ assert_empty t.errors[:content]
t.title = "-11"
assert t.invalid?, "Shouldn't be valid"
@@ -58,14 +58,14 @@ class PresenceValidationTest < ActiveModel::TestCase
t.title = "1"
- assert t.valid?
- assert t.errors[:title].empty?
+ assert_predicate t, :valid?
+ assert_empty t.errors[:title]
end
def test_validate_format_with_formatted_message
Topic.validates_format_of(:title, with: /\AValid Title\z/, message: "can't be %{value}")
t = Topic.new(title: "Invalid title")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["can't be Invalid title"], t.errors[:title]
end
@@ -114,10 +114,10 @@ class PresenceValidationTest < ActiveModel::TestCase
t = Topic.new
t.title = "digit"
t.content = "Pixies"
- assert t.invalid?
+ assert_predicate t, :invalid?
t.content = "1234"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_format_of_without_lambda
@@ -126,10 +126,10 @@ class PresenceValidationTest < ActiveModel::TestCase
t = Topic.new
t.title = "characters"
t.content = "1234"
- assert t.invalid?
+ assert_predicate t, :invalid?
t.content = "Pixies"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_format_of_for_ruby_class
@@ -137,12 +137,12 @@ class PresenceValidationTest < ActiveModel::TestCase
p = Person.new
p.karma = "Pixies"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is invalid"], p.errors[:karma]
p.karma = "1234"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb
index 94df0649a9..daad76759f 100644
--- a/activemodel/test/cases/validations/inclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/inclusion_validation_test.rb
@@ -13,61 +13,61 @@ class InclusionValidationTest < ActiveModel::TestCase
def test_validates_inclusion_of_range
Topic.validates_inclusion_of(:title, in: "aaa".."bbb")
- assert Topic.new("title" => "bbc", "content" => "abc").invalid?
- assert Topic.new("title" => "aa", "content" => "abc").invalid?
- assert Topic.new("title" => "aaab", "content" => "abc").invalid?
- assert Topic.new("title" => "aaa", "content" => "abc").valid?
- assert Topic.new("title" => "abc", "content" => "abc").valid?
- assert Topic.new("title" => "bbb", "content" => "abc").valid?
+ assert_predicate Topic.new("title" => "bbc", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => "aa", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => "aaab", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => "aaa", "content" => "abc"), :valid?
+ assert_predicate Topic.new("title" => "abc", "content" => "abc"), :valid?
+ assert_predicate Topic.new("title" => "bbb", "content" => "abc"), :valid?
end
def test_validates_inclusion_of_time_range
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?
+ assert_predicate Topic.new(title: "aaa", created_at: 2.years.ago), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: 3.months.ago), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.from_now), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid?
end
def test_validates_inclusion_of_date_range
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?
+ assert_predicate Topic.new(title: "aaa", created_at: 2.years.until(Date.today)), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: 3.months.until(Date.today)), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.since(Date.today)), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: 1.year.until(Date.today)), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: Date.today), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid?
end
def test_validates_inclusion_of_date_time_range
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?
+ assert_predicate Topic.new(title: "aaa", created_at: 2.years.until(DateTime.current)), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: 3.months.until(DateTime.current)), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.since(DateTime.current)), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid?
end
def test_validates_inclusion_of
Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ))
- assert Topic.new("title" => "a!", "content" => "abc").invalid?
- assert Topic.new("title" => "a b", "content" => "abc").invalid?
- assert Topic.new("title" => nil, "content" => "def").invalid?
+ assert_predicate Topic.new("title" => "a!", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => "a b", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => nil, "content" => "def"), :invalid?
t = Topic.new("title" => "a", "content" => "I know you are but what am I?")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "uhoh"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is not included in the list"], t.errors[:title]
assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: nil) }
@@ -81,30 +81,30 @@ class InclusionValidationTest < ActiveModel::TestCase
def test_validates_inclusion_of_with_allow_nil
Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), allow_nil: true)
- assert Topic.new("title" => "a!", "content" => "abc").invalid?
- assert Topic.new("title" => "", "content" => "abc").invalid?
- assert Topic.new("title" => nil, "content" => "abc").valid?
+ assert_predicate Topic.new("title" => "a!", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => "", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => nil, "content" => "abc"), :valid?
end
def test_validates_inclusion_of_with_formatted_message
Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), message: "option %{value} is not in the list")
- assert Topic.new("title" => "a", "content" => "abc").valid?
+ assert_predicate Topic.new("title" => "a", "content" => "abc"), :valid?
t = Topic.new("title" => "uhoh", "content" => "abc")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["option uhoh is not in the list"], t.errors[:title]
end
def test_validates_inclusion_of_with_within_option
Topic.validates_inclusion_of(:title, within: %w( a b c d e f g ))
- assert Topic.new("title" => "a", "content" => "abc").valid?
+ assert_predicate Topic.new("title" => "a", "content" => "abc"), :valid?
t = Topic.new("title" => "uhoh", "content" => "abc")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
end
def test_validates_inclusion_of_for_ruby_class
@@ -112,12 +112,12 @@ class InclusionValidationTest < ActiveModel::TestCase
p = Person.new
p.karma = "Lifo"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is not included in the list"], p.errors[:karma]
p.karma = "monkey"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
@@ -128,10 +128,10 @@ class InclusionValidationTest < ActiveModel::TestCase
t = Topic.new
t.title = "wasabi"
t.author_name = "sikachu"
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title = "elephant"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_inclusion_of_with_symbol
@@ -144,7 +144,7 @@ class InclusionValidationTest < ActiveModel::TestCase
%w()
end
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is not included in the list"], p.errors[:karma]
p = Person.new
@@ -154,7 +154,7 @@ class InclusionValidationTest < ActiveModel::TestCase
%w(Lifo)
end
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb
index a0d8e058f5..774a2cde74 100644
--- a/activemodel/test/cases/validations/length_validation_test.rb
+++ b/activemodel/test/cases/validations/length_validation_test.rb
@@ -13,153 +13,153 @@ class LengthValidationTest < ActiveModel::TestCase
def test_validates_length_of_with_allow_nil
Topic.validates_length_of(:title, is: 5, allow_nil: true)
- assert Topic.new("title" => "ab").invalid?
- assert Topic.new("title" => "").invalid?
- assert Topic.new("title" => nil).valid?
- assert Topic.new("title" => "abcde").valid?
+ assert_predicate Topic.new("title" => "ab"), :invalid?
+ assert_predicate Topic.new("title" => ""), :invalid?
+ assert_predicate Topic.new("title" => nil), :valid?
+ assert_predicate Topic.new("title" => "abcde"), :valid?
end
def test_validates_length_of_with_allow_blank
Topic.validates_length_of(:title, is: 5, allow_blank: true)
- assert Topic.new("title" => "ab").invalid?
- assert Topic.new("title" => "").valid?
- assert Topic.new("title" => nil).valid?
- assert Topic.new("title" => "abcde").valid?
+ assert_predicate Topic.new("title" => "ab"), :invalid?
+ assert_predicate Topic.new("title" => ""), :valid?
+ assert_predicate Topic.new("title" => nil), :valid?
+ assert_predicate Topic.new("title" => "abcde"), :valid?
end
def test_validates_length_of_using_minimum
Topic.validates_length_of :title, minimum: 5
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "not"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title]
t.title = ""
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title]
t.title = nil
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"]
end
def test_validates_length_of_using_maximum_should_allow_nil
Topic.validates_length_of :title, maximum: 10
t = Topic.new
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_optionally_validates_length_of_using_minimum
Topic.validates_length_of :title, minimum: 5, allow_nil: true
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = nil
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_maximum
Topic.validates_length_of :title, maximum: 5
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "notvalid"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title]
t.title = ""
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_optionally_validates_length_of_using_maximum
Topic.validates_length_of :title, maximum: 5, allow_nil: true
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = nil
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_within
Topic.validates_length_of(:title, :content, within: 3..5)
t = Topic.new("title" => "a!", "content" => "I'm ooooooooh so very long")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title]
assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content]
t.title = nil
t.content = nil
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title]
assert_equal ["is too short (minimum is 3 characters)"], t.errors[:content]
t.title = "abe"
t.content = "mad"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_within_with_exclusive_range
Topic.validates_length_of(:title, within: 4...10)
t = Topic.new("title" => "9 chars!!")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "Now I'm 10"
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["is too long (maximum is 9 characters)"], t.errors[:title]
t.title = "Four"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_optionally_validates_length_of_using_within
Topic.validates_length_of :title, :content, within: 3..5, allow_nil: true
t = Topic.new("title" => "abc", "content" => "abcd")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = nil
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_is
Topic.validates_length_of :title, is: 5
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "notvalid"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is the wrong length (should be 5 characters)"], t.errors[:title]
t.title = ""
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title = nil
- assert t.invalid?
+ assert_predicate t, :invalid?
end
def test_optionally_validates_length_of_using_is
Topic.validates_length_of :title, is: 5, allow_nil: true
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = nil
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_bignum
@@ -187,45 +187,45 @@ class LengthValidationTest < ActiveModel::TestCase
def test_validates_length_of_custom_errors_for_minimum_with_message
Topic.validates_length_of(:title, minimum: 5, message: "boo %{count}")
t = Topic.new("title" => "uhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["boo 5"], t.errors[:title]
end
def test_validates_length_of_custom_errors_for_minimum_with_too_short
Topic.validates_length_of(:title, minimum: 5, too_short: "hoo %{count}")
t = Topic.new("title" => "uhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors[:title]
end
def test_validates_length_of_custom_errors_for_maximum_with_message
Topic.validates_length_of(:title, maximum: 5, message: "boo %{count}")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["boo 5"], t.errors[:title]
end
def test_validates_length_of_custom_errors_for_in
Topic.validates_length_of(:title, in: 10..20, message: "hoo %{count}")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 10"], t.errors["title"]
t = Topic.new("title" => "uhohuhohuhohuhohuhohuhohuhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 20"], t.errors["title"]
end
def test_validates_length_of_custom_errors_for_maximum_with_too_long
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors["title"]
end
@@ -233,29 +233,29 @@ class LengthValidationTest < ActiveModel::TestCase
Topic.validates_length_of :title, minimum: 3, maximum: 5, too_short: "too short", too_long: "too long"
t = Topic.new(title: "a")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["too short"], t.errors["title"]
t = Topic.new(title: "aaaaaa")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["too long"], t.errors["title"]
end
def test_validates_length_of_custom_errors_for_is_with_message
Topic.validates_length_of(:title, is: 5, message: "boo %{count}")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["boo 5"], t.errors["title"]
end
def test_validates_length_of_custom_errors_for_is_with_wrong_length
Topic.validates_length_of(:title, is: 5, wrong_length: "hoo %{count}")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors["title"]
end
@@ -263,11 +263,11 @@ class LengthValidationTest < ActiveModel::TestCase
Topic.validates_length_of :title, minimum: 5
t = Topic.new("title" => "一二三四五", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "一二三四"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"]
end
@@ -275,11 +275,11 @@ class LengthValidationTest < ActiveModel::TestCase
Topic.validates_length_of :title, maximum: 5
t = Topic.new("title" => "一二三四五", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "一二34五六"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is too long (maximum is 5 characters)"], t.errors["title"]
end
@@ -287,12 +287,12 @@ class LengthValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, :content, within: 3..5)
t = Topic.new("title" => "一二", "content" => "12三四五六七")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title]
assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content]
t.title = "一二三"
t.content = "12三"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_optionally_validates_length_of_using_within_utf8
@@ -312,11 +312,11 @@ class LengthValidationTest < ActiveModel::TestCase
Topic.validates_length_of :title, is: 5
t = Topic.new("title" => "一二345", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "一二345六"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is the wrong length (should be 5 characters)"], t.errors["title"]
end
@@ -324,11 +324,11 @@ class LengthValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:approved, is: 4)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1)
- assert t.invalid?
- assert t.errors[:approved].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:approved], :any?
t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1234)
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_for_ruby_class
@@ -336,12 +336,12 @@ class LengthValidationTest < ActiveModel::TestCase
p = Person.new
p.karma = "Pix"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is too short (minimum is 5 characters)"], p.errors[:karma]
p.karma = "The Smiths"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
@@ -350,64 +350,95 @@ class LengthValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, within: 5..Float::INFINITY)
t = Topic.new("title" => "1234")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
t.title = "12345"
- assert t.valid?
+ assert_predicate t, :valid?
Topic.validates_length_of(:author_name, maximum: Float::INFINITY)
- assert t.valid?
+ assert_predicate t, :valid?
t.author_name = "A very long author name that should still be valid." * 100
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_maximum_should_not_allow_nil_when_nil_not_allowed
Topic.validates_length_of :title, maximum: 10, allow_nil: false
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
end
def test_validates_length_of_using_maximum_should_not_allow_nil_and_empty_string_when_blank_not_allowed
Topic.validates_length_of :title, maximum: 10, allow_blank: false
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title = ""
- assert t.invalid?
+ assert_predicate t, :invalid?
end
def test_validates_length_of_using_both_minimum_and_maximum_should_not_allow_nil
Topic.validates_length_of :title, minimum: 5, maximum: 10
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
end
def test_validates_length_of_using_minimum_0_should_not_allow_nil
Topic.validates_length_of :title, minimum: 0
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title = ""
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_is_0_should_not_allow_nil
Topic.validates_length_of :title, is: 0
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title = ""
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_with_diff_in_option
Topic.validates_length_of(:title, is: 5)
Topic.validates_length_of(:title, is: 5, if: Proc.new { false })
- assert Topic.new("title" => "david").valid?
- assert Topic.new("title" => "david2").invalid?
+ assert_predicate Topic.new("title" => "david"), :valid?
+ assert_predicate Topic.new("title" => "david2"), :invalid?
+ end
+
+ def test_validates_length_of_using_proc_as_maximum
+ Topic.validates_length_of :title, maximum: ->(model) { 5 }
+
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert_predicate t, :valid?
+
+ t.title = "notvalid"
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
+ assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title]
+
+ t.title = ""
+ assert_predicate t, :valid?
+ end
+
+ def test_validates_length_of_using_proc_as_maximum_with_model_method
+ Topic.send(:define_method, :max_title_length, lambda { 5 })
+ Topic.validates_length_of :title, maximum: Proc.new(&:max_title_length)
+
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert_predicate t, :valid?
+
+ t.title = "notvalid"
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
+ assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title]
+
+ t.title = ""
+ assert_predicate t, :valid?
end
end
diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb
index 001815e28f..01b78ae72e 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -20,7 +20,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090)
FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS
INTEGERS = [0, 10, -10] + INTEGER_STRINGS
- BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal.new(bd) }
+ BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal(bd) }
JUNK = ["not a number", "42 not a number", "0xdeadbeef", "0xinvalidhex", "0Xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number"]
INFINITY = [1.0 / 0.0]
@@ -59,7 +59,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_of_with_integer_only_and_symbol_as_value
- Topic.validates_numericality_of :approved, only_integer: :condition_is_true_but_its_not
+ Topic.validates_numericality_of :approved, only_integer: :condition_is_false
invalid!(NIL + BLANK + JUNK)
valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
@@ -81,10 +81,10 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_with_greater_than_using_differing_numeric_types
- Topic.validates_numericality_of :approved, greater_than: BigDecimal.new("97.18")
+ Topic.validates_numericality_of :approved, greater_than: BigDecimal("97.18")
- invalid!([-97.18, BigDecimal.new("97.18"), BigDecimal("-97.18")], "must be greater than 97.18")
- valid!([97.19, 98, BigDecimal.new("98"), BigDecimal.new("97.19")])
+ invalid!([-97.18, BigDecimal("97.18"), BigDecimal("-97.18")], "must be greater than 97.18")
+ valid!([97.19, 98, BigDecimal("98"), BigDecimal("97.19")])
end
def test_validates_numericality_with_greater_than_using_string_value
@@ -102,10 +102,10 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_with_greater_than_or_equal_using_differing_numeric_types
- Topic.validates_numericality_of :approved, greater_than_or_equal_to: BigDecimal.new("97.18")
+ Topic.validates_numericality_of :approved, greater_than_or_equal_to: BigDecimal("97.18")
- invalid!([-97.18, 97.17, 97, BigDecimal.new("97.17"), BigDecimal.new("-97.18")], "must be greater than or equal to 97.18")
- valid!([97.18, 98, BigDecimal.new("97.19")])
+ invalid!([-97.18, 97.17, 97, BigDecimal("97.17"), BigDecimal("-97.18")], "must be greater than or equal to 97.18")
+ valid!([97.18, 98, BigDecimal("97.19")])
end
def test_validates_numericality_with_greater_than_or_equal_using_string_value
@@ -123,10 +123,10 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_with_equal_to_using_differing_numeric_types
- Topic.validates_numericality_of :approved, equal_to: BigDecimal.new("97.18")
+ Topic.validates_numericality_of :approved, equal_to: BigDecimal("97.18")
invalid!([-97.18], "must be equal to 97.18")
- valid!([BigDecimal.new("97.18")])
+ valid!([BigDecimal("97.18")])
end
def test_validates_numericality_with_equal_to_using_string_value
@@ -144,10 +144,10 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_with_less_than_using_differing_numeric_types
- Topic.validates_numericality_of :approved, less_than: BigDecimal.new("97.18")
+ Topic.validates_numericality_of :approved, less_than: BigDecimal("97.18")
- invalid!([97.18, BigDecimal.new("97.18")], "must be less than 97.18")
- valid!([-97.0, 97.0, -97, 97, BigDecimal.new("-97"), BigDecimal.new("97")])
+ invalid!([97.18, BigDecimal("97.18")], "must be less than 97.18")
+ valid!([-97.0, 97.0, -97, 97, BigDecimal("-97"), BigDecimal("97")])
end
def test_validates_numericality_with_less_than_using_string_value
@@ -165,10 +165,10 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_with_less_than_or_equal_to_using_differing_numeric_types
- Topic.validates_numericality_of :approved, less_than_or_equal_to: BigDecimal.new("97.18")
+ Topic.validates_numericality_of :approved, less_than_or_equal_to: BigDecimal("97.18")
invalid!([97.19, 98], "must be less than or equal to 97.18")
- valid!([-97.18, BigDecimal.new("-97.18"), BigDecimal.new("97.18")])
+ valid!([-97.18, BigDecimal("-97.18"), BigDecimal("97.18")])
end
def test_validates_numericality_with_less_than_or_equal_using_string_value
@@ -237,13 +237,13 @@ class NumericalityValidationTest < ActiveModel::TestCase
Topic.validates_numericality_of :approved, less_than: 4, message: "smaller than %{count}"
topic = Topic.new("title" => "numeric test", "approved" => 10)
- assert !topic.valid?
+ assert_not_predicate topic, :valid?
assert_equal ["smaller than 4"], topic.errors[:approved]
Topic.validates_numericality_of :approved, greater_than: 4, message: "greater than %{count}"
topic = Topic.new("title" => "numeric test", "approved" => 1)
- assert !topic.valid?
+ assert_not_predicate topic, :valid?
assert_equal ["greater than 4"], topic.errors[:approved]
end
@@ -252,12 +252,12 @@ class NumericalityValidationTest < ActiveModel::TestCase
p = Person.new
p.karma = "Pix"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is not a number"], p.errors[:karma]
p.karma = "1234"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
@@ -268,7 +268,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
topic = Topic.new
topic.approved = (base + 1).to_s
- assert topic.invalid?
+ assert_predicate topic, :invalid?
end
def test_validates_numericality_with_invalid_args
diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb
index 22c2f0af87..c3eca41070 100644
--- a/activemodel/test/cases/validations/presence_validation_test.rb
+++ b/activemodel/test/cases/validations/presence_validation_test.rb
@@ -17,25 +17,25 @@ class PresenceValidationTest < ActiveModel::TestCase
Topic.validates_presence_of(:title, :content)
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["can't be blank"], t.errors[:title]
assert_equal ["can't be blank"], t.errors[:content]
t.title = "something"
t.content = " "
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["can't be blank"], t.errors[:content]
t.content = "like stuff"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_accepts_array_arguments
Topic.validates_presence_of %w(title content)
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["can't be blank"], t.errors[:title]
assert_equal ["can't be blank"], t.errors[:content]
end
@@ -43,7 +43,7 @@ class PresenceValidationTest < ActiveModel::TestCase
def test_validates_acceptance_of_with_custom_error_using_quotes
Person.validates_presence_of :karma, message: "This string contains 'single' and \"double\" quotes"
p = Person.new
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last
end
@@ -51,24 +51,24 @@ class PresenceValidationTest < ActiveModel::TestCase
Person.validates_presence_of :karma
p = Person.new
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["can't be blank"], p.errors[:karma]
p.karma = "Cold"
- assert p.valid?
+ assert_predicate p, :valid?
end
def test_validates_presence_of_for_ruby_class_with_custom_reader
CustomReader.validates_presence_of :karma
p = CustomReader.new
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["can't be blank"], p.errors[:karma]
p[:karma] = "Cold"
- assert p.valid?
+ assert_predicate p, :valid?
end
def test_validates_presence_of_with_allow_nil_option
@@ -78,7 +78,7 @@ class PresenceValidationTest < ActiveModel::TestCase
assert t.valid?, t.errors.full_messages
t.title = ""
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["can't be blank"], t.errors[:title]
t.title = " "
diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb
index 77cb8ebdc1..80c347703a 100644
--- a/activemodel/test/cases/validations/validates_test.rb
+++ b/activemodel/test/cases/validations/validates_test.rb
@@ -37,7 +37,7 @@ class ValidatesTest < ActiveModel::TestCase
person = Person.new
person.title = 123
- assert person.valid?
+ assert_predicate person, :valid?
end
def test_validates_with_built_in_validation_and_options
@@ -62,56 +62,62 @@ class ValidatesTest < ActiveModel::TestCase
end
def test_validates_with_if_as_local_conditions
- Person.validates :karma, presence: true, email: { unless: :condition_is_true }
+ Person.validates :karma, presence: true, email: { if: :condition_is_false }
person = Person.new
person.valid?
assert_equal ["can't be blank"], person.errors[:karma]
end
def test_validates_with_if_as_shared_conditions
- Person.validates :karma, presence: true, email: true, if: :condition_is_true
+ Person.validates :karma, presence: true, email: true, if: :condition_is_false
+ person = Person.new
+ assert_predicate person, :valid?
+ end
+
+ def test_validates_with_unless_as_local_conditions
+ Person.validates :karma, presence: true, email: { unless: :condition_is_true }
person = Person.new
person.valid?
- assert_equal ["can't be blank", "is not an email"], person.errors[:karma].sort
+ assert_equal ["can't be blank"], person.errors[:karma]
end
def test_validates_with_unless_shared_conditions
Person.validates :karma, presence: true, email: true, unless: :condition_is_true
person = Person.new
- assert person.valid?
+ assert_predicate person, :valid?
end
def test_validates_with_allow_nil_shared_conditions
Person.validates :karma, length: { minimum: 20 }, email: true, allow_nil: true
person = Person.new
- assert person.valid?
+ assert_predicate person, :valid?
end
def test_validates_with_regexp
Person.validates :karma, format: /positive|negative/
person = Person.new
- assert person.invalid?
+ assert_predicate person, :invalid?
assert_equal ["is invalid"], person.errors[:karma]
person.karma = "positive"
- assert person.valid?
+ assert_predicate person, :valid?
end
def test_validates_with_array
Person.validates :gender, inclusion: %w(m f)
person = Person.new
- assert person.invalid?
+ assert_predicate person, :invalid?
assert_equal ["is not included in the list"], person.errors[:gender]
person.gender = "m"
- assert person.valid?
+ assert_predicate person, :valid?
end
def test_validates_with_range
Person.validates :karma, length: 6..20
person = Person.new
- assert person.invalid?
+ assert_predicate person, :invalid?
assert_equal ["is too short (minimum is 6 characters)"], person.errors[:karma]
person.karma = "something"
- assert person.valid?
+ assert_predicate person, :valid?
end
def test_validates_with_validator_class_and_options
@@ -153,7 +159,7 @@ class ValidatesTest < ActiveModel::TestCase
topic = Topic.new
topic.title = "What's happening"
topic.title_confirmation = "Not this"
- assert !topic.valid?
+ assert_not_predicate topic, :valid?
assert_equal ["Y U NO CONFIRM"], topic.errors[:title_confirmation]
end
end
diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb
index fbe20dc000..8239792c79 100644
--- a/activemodel/test/cases/validations/with_validation_test.rb
+++ b/activemodel/test/cases/validations/with_validation_test.rb
@@ -65,71 +65,35 @@ class ValidatesWithTest < ActiveModel::TestCase
test "with multiple classes" do
Topic.validates_with(ValidatorThatAddsErrors, OtherValidatorThatAddsErrors)
topic = Topic.new
- assert topic.invalid?
+ assert_predicate topic, :invalid?
assert_includes topic.errors[:base], ERROR_MESSAGE
assert_includes topic.errors[:base], OTHER_ERROR_MESSAGE
end
- test "with if statements that return false" do
- ActiveSupport::Deprecation.silence do
- Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 2")
- end
- topic = Topic.new
- assert topic.valid?
- end
-
- test "with if statements that return true" do
- ActiveSupport::Deprecation.silence do
- Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 1")
- end
- topic = Topic.new
- assert topic.invalid?
- assert_includes topic.errors[:base], ERROR_MESSAGE
- end
-
- test "with unless statements that return true" do
- ActiveSupport::Deprecation.silence do
- Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 1")
- end
- topic = Topic.new
- assert topic.valid?
- end
-
- test "with unless statements that returns false" do
- ActiveSupport::Deprecation.silence do
- Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 2")
- end
- topic = Topic.new
- assert topic.invalid?
- assert_includes topic.errors[:base], ERROR_MESSAGE
- end
-
test "passes all configuration options to the validator class" do
topic = Topic.new
validator = Minitest::Mock.new
- validator.expect(:new, validator, [{ foo: :bar, if: "1 == 1", class: Topic }])
+ validator.expect(:new, validator, [{ foo: :bar, if: :condition_is_true, class: Topic }])
validator.expect(:validate, nil, [topic])
validator.expect(:is_a?, false, [Symbol])
validator.expect(:is_a?, false, [String])
- ActiveSupport::Deprecation.silence do
- Topic.validates_with(validator, if: "1 == 1", foo: :bar)
- end
- assert topic.valid?
+ Topic.validates_with(validator, if: :condition_is_true, foo: :bar)
+ assert_predicate topic, :valid?
validator.verify
end
test "validates_with with options" do
Topic.validates_with(ValidatorThatValidatesOptions, field: :first_name)
topic = Topic.new
- assert topic.invalid?
+ assert_predicate topic, :invalid?
assert_includes topic.errors[:base], ERROR_MESSAGE
end
test "validates_with each validator" do
Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content])
topic = Topic.new title: "Title", content: "Content"
- assert topic.invalid?
+ assert_predicate topic, :invalid?
assert_equal ["Value is Title"], topic.errors[:title]
assert_equal ["Value is Content"], topic.errors[:content]
end
@@ -149,28 +113,28 @@ class ValidatesWithTest < ActiveModel::TestCase
test "each validator skip nil values if :allow_nil is set to true" do
Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content], allow_nil: true)
topic = Topic.new content: ""
- assert topic.invalid?
- assert topic.errors[:title].empty?
+ assert_predicate topic, :invalid?
+ assert_empty topic.errors[:title]
assert_equal ["Value is "], topic.errors[:content]
end
test "each validator skip blank values if :allow_blank is set to true" do
Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content], allow_blank: true)
topic = Topic.new content: ""
- assert topic.valid?
- assert topic.errors[:title].empty?
- assert topic.errors[:content].empty?
+ assert_predicate topic, :valid?
+ assert_empty topic.errors[:title]
+ assert_empty topic.errors[:content]
end
test "validates_with can validate with an instance method" do
Topic.validates :title, with: :my_validation
topic = Topic.new title: "foo"
- assert topic.valid?
- assert topic.errors[:title].empty?
+ assert_predicate topic, :valid?
+ assert_empty topic.errors[:title]
topic = Topic.new
- assert !topic.valid?
+ assert_not_predicate topic, :valid?
assert_equal ["is missing"], topic.errors[:title]
end
@@ -178,8 +142,8 @@ class ValidatesWithTest < ActiveModel::TestCase
Topic.validates :title, :content, with: :my_validation_with_arg
topic = Topic.new title: "foo"
- assert !topic.valid?
- assert topic.errors[:title].empty?
+ assert_not_predicate topic, :valid?
+ assert_empty topic.errors[:title]
assert_equal ["is missing"], topic.errors[:content]
end
end
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb
index ab8c41bbd0..7776233db5 100644
--- a/activemodel/test/cases/validations_test.rb
+++ b/activemodel/test/cases/validations_test.rb
@@ -30,7 +30,7 @@ class ValidationsTest < ActiveModel::TestCase
def test_single_attr_validation_and_error_msg
r = Reply.new
r.title = "There's no content!"
- assert r.invalid?
+ assert_predicate r, :invalid?
assert r.errors[:content].any?, "A reply without content should mark that attribute as invalid"
assert_equal ["is Empty"], r.errors["content"], "A reply without content should contain an error"
assert_equal 1, r.errors.count
@@ -38,7 +38,7 @@ class ValidationsTest < ActiveModel::TestCase
def test_double_attr_validation_and_error_msg
r = Reply.new
- assert r.invalid?
+ assert_predicate r, :invalid?
assert r.errors[:title].any?, "A reply without title should mark that attribute as invalid"
assert_equal ["is Empty"], r.errors["title"], "A reply without title should contain an error"
@@ -111,8 +111,8 @@ class ValidationsTest < ActiveModel::TestCase
def test_errors_empty_after_errors_on_check
t = Topic.new
- assert t.errors[:id].empty?
- assert t.errors.empty?
+ assert_empty t.errors[:id]
+ assert_empty t.errors
end
def test_validates_each
@@ -122,7 +122,7 @@ class ValidationsTest < ActiveModel::TestCase
hits += 1
end
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal 4, hits
assert_equal %w(gotcha gotcha), t.errors[:title]
assert_equal %w(gotcha gotcha), t.errors[:content]
@@ -135,7 +135,7 @@ class ValidationsTest < ActiveModel::TestCase
hits += 1
end
t = CustomReader.new("title" => "valid", "content" => "whatever")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal 4, hits
assert_equal %w(gotcha gotcha), t.errors[:title]
assert_equal %w(gotcha gotcha), t.errors[:content]
@@ -146,16 +146,16 @@ class ValidationsTest < ActiveModel::TestCase
def test_validate_block
Topic.validate { errors.add("title", "will never be valid") }
t = Topic.new("title" => "Title", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["will never be valid"], t.errors["title"]
end
def test_validate_block_with_params
Topic.validate { |topic| topic.errors.add("title", "will never be valid") }
t = Topic.new("title" => "Title", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["will never be valid"], t.errors["title"]
end
@@ -214,7 +214,7 @@ class ValidationsTest < ActiveModel::TestCase
def test_errors_conversions
Topic.validates_presence_of %w(title content)
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
xml = t.errors.to_xml
assert_match %r{<errors>}, xml
@@ -232,14 +232,14 @@ class ValidationsTest < ActiveModel::TestCase
Topic.validates_length_of :title, minimum: 2
t = Topic.new("title" => "")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal "can't be blank", t.errors["title"].first
Topic.validates_presence_of :title, :author_name
Topic.validate { errors.add("author_email_address", "will never be valid") }
Topic.validates_length_of :title, :content, minimum: 2
t = Topic.new title: ""
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal :title, key = t.errors.keys[0]
assert_equal "can't be blank", t.errors[key][0]
@@ -258,8 +258,8 @@ class ValidationsTest < ActiveModel::TestCase
t = Topic.new(title: "")
# If block should not fire
- assert t.valid?
- assert t.author_name.nil?
+ assert_predicate t, :valid?
+ assert_predicate t.author_name, :nil?
# If block should fire
assert t.invalid?(:update)
@@ -270,18 +270,18 @@ class ValidationsTest < ActiveModel::TestCase
Topic.validates_presence_of :title
t = Topic.new
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
t.title = "Things are going to change"
- assert !t.invalid?
+ assert_not_predicate t, :invalid?
end
def test_validation_with_message_as_proc
Topic.validates_presence_of(:title, message: proc { "no blanks here".upcase })
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["NO BLANKS HERE"], t.errors[:title]
end
@@ -331,13 +331,13 @@ class ValidationsTest < ActiveModel::TestCase
Topic.validates :content, length: { minimum: 10 }
topic = Topic.new
- assert topic.invalid?
+ assert_predicate topic, :invalid?
assert_equal 3, topic.errors.size
topic.title = "Some Title"
topic.author_name = "Some Author"
topic.content = "Some Content Whose Length is more than 10."
- assert topic.valid?
+ assert_predicate topic, :valid?
end
def test_validate
@@ -381,7 +381,7 @@ class ValidationsTest < ActiveModel::TestCase
def test_strict_validation_not_fails
Topic.validates :title, strict: true, presence: true
- assert Topic.new(title: "hello").valid?
+ assert_predicate Topic.new(title: "hello"), :valid?
end
def test_strict_validation_particular_validator
@@ -414,7 +414,7 @@ class ValidationsTest < ActiveModel::TestCase
def test_validates_with_false_hash_value
Topic.validates :title, presence: false
- assert Topic.new.valid?
+ assert_predicate Topic.new, :valid?
end
def test_strict_validation_error_message
@@ -439,19 +439,19 @@ class ValidationsTest < ActiveModel::TestCase
duped = topic.dup
duped.title = nil
- assert duped.invalid?
+ assert_predicate duped, :invalid?
topic.title = nil
duped.title = "Mathematics"
- assert topic.invalid?
- assert duped.valid?
+ assert_predicate topic, :invalid?
+ assert_predicate 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_predicate t, :invalid?
assert_equal ["You have failed me for the last time, Admiral."], t.errors[:title]
end
@@ -459,7 +459,7 @@ class ValidationsTest < ActiveModel::TestCase
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_predicate t, :invalid?
assert_equal ["Title is missing. You have failed me for the last time, Admiral."], t.errors[:title]
end
end
diff --git a/activemodel/test/models/person.rb b/activemodel/test/models/person.rb
index b61fdf76b1..8dd8ceadad 100644
--- a/activemodel/test/models/person.rb
+++ b/activemodel/test/models/person.rb
@@ -9,6 +9,10 @@ class Person
def condition_is_true
true
end
+
+ def condition_is_false
+ false
+ end
end
class Person::Gender
diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb
index 2f4e92c3b2..b0af00ee45 100644
--- a/activemodel/test/models/topic.rb
+++ b/activemodel/test/models/topic.rb
@@ -23,7 +23,7 @@ class Topic
true
end
- def condition_is_true_but_its_not
+ def condition_is_false
false
end
diff --git a/activerecord/.gitignore b/activerecord/.gitignore
new file mode 100644
index 0000000000..884ee009eb
--- /dev/null
+++ b/activerecord/.gitignore
@@ -0,0 +1,3 @@
+/sqlnet.log
+/test/config.yml
+/test/fixtures/*.sqlite*
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index b421fedc96..36a3d59784 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,242 +1,44 @@
-* Add new error class `TransactionTimeout` for MySQL adapter which will be raised
- when lock wait time expires.
+* Ensure `Associations::CollectionAssociation#size` and `Associations::CollectionAssociation#empty?`
+ use loaded association ids if present.
- *Gabriel Courtemanche*
+ *Graham Turner*
-* Remove deprecated `#migration_keys`.
+* Add support to preload associations of polymorphic associations when not all the records have the requested associations.
- *Ryuta Kamizono*
+ *Dana Sherson*
-* Automatically guess the inverse associations for STI.
+* Add `touch_all` method to `ActiveRecord::Relation`.
- *Yuichiro Kaneko*
+ Example:
-* Ensure `sum` honors `distinct` on `has_many :through` associations
+ Person.where(name: "David").touch_all(time: Time.new(2020, 5, 16, 0, 0, 0))
- Fixes #16791.
+ *fatkodima*, *duggiefresh*
- *Aaron Wortham*
+* Add `ActiveRecord::Base.base_class?` predicate.
-* Add `binary` fixture helper method.
+ *Bogdan Gusiev*
- *Atsushi Yoshida*
+* Add custom prefix option to ActiveRecord::Store.store_accessor.
-* When using `Relation#or`, extract the common conditions and put them before the OR condition.
+ *Tan Huynh*
- *Maxime Handfield Lapointe*
+* Rails 6 requires Ruby 2.4.1 or newer.
-* `Relation#or` now accepts two relations who have different values for
- `references` only, as `references` can be implicitly called by `where`.
+ *Jeremy Daer*
- Fixes #29411.
+* Deprecate `update_attributes`/`!` in favor of `update`/`!`.
- *Sean Griffin*
+ *Eddie Lebow*
-* `ApplicationRecord` is no longer generated when generating models. If you
- need to generate it, it can be created with `rails g application_record`.
-
- *Lisa Ugray*
-
-* Fix `COUNT(DISTINCT ...)` with `ORDER BY` and `LIMIT` to keep the existing select list.
-
- *Ryuta Kamizono*
-
-* When a `has_one` association is destroyed by `dependent: destroy`,
- `destroyed_by_association` will now be set to the reflection, matching the
- behaviour of `has_many` associations.
-
- *Lisa Ugray*
-
-* Fix `unscoped(where: [columns])` removing the wrong bind values
-
- When the `where` is called on a relation after a `or`, unscoping the column of that later `where` removed
- bind values used by the `or` instead. (possibly other cases too)
-
- ```
- Post.where(id: 1).or(Post.where(id: 2)).where(foo: 3).unscope(where: :foo).to_sql
- # Currently:
- # SELECT "posts".* FROM "posts" WHERE ("posts"."id" = 2 OR "posts"."id" = 3)
- # With fix:
- # SELECT "posts".* FROM "posts" WHERE ("posts"."id" = 1 OR "posts"."id" = 2)
- ```
-
- *Maxime Handfield Lapointe*
-
-* Values constructed using multi-parameter assignment will now use the
- post-type-cast value for rendering in single-field form inputs.
-
- *Sean Griffin*
-
-* `Relation#joins` is no longer affected by the target model's
- `current_scope`, with the exception of `unscoped`.
-
- Fixes #29338.
-
- *Sean Griffin*
-
-* Change sqlite3 boolean serialization to use 1 and 0
-
- SQLite natively recognizes 1 and 0 as true and false, but does not natively
- recognize 't' and 'f' as was previously serialized.
-
- This change in serialization requires a migration of stored boolean data
- for SQLite databases, so it's implemented behind a configuration flag
- whose default false value is deprecated.
-
- *Lisa Ugray*
-
-* Skip query caching when working with batches of records (`find_each`, `find_in_batches`,
- `in_batches`).
-
- Previously, records would be fetched in batches, but all records would be retained in memory
- until the end of the request or job.
-
- *Eugene Kenny*
-
-* Prevent errors raised by `sql.active_record` notification subscribers from being converted into
- `ActiveRecord::StatementInvalid` exceptions.
-
- *Dennis Taylor*
-
-* Fix eager loading/preloading association with scope including joins.
-
- Fixes #28324.
-
- *Ryuta Kamizono*
-
-* Fix transactions to apply state to child transactions
-
- Previously, if you had a nested transaction and the outer transaction was rolledback, the record from the
- inner transaction would still be marked as persisted.
-
- This change fixes that by applying the state of the parent transaction to the child transaction when the
- parent transaction is rolledback. This will correctly mark records from the inner transaction as not persisted.
-
- *Eileen M. Uchitelle*, *Aaron Patterson*
-
-* Deprecate `set_state` method in `TransactionState`
-
- Deprecated the `set_state` method in favor of setting the state via specific methods. If you need to mark the
- state of the transaction you can now use `rollback!`, `commit!` or `nullify!` instead of
- `set_state(:rolledback)`, `set_state(:committed)`, or `set_state(nil)`.
-
- *Eileen M. Uchitelle*, *Aaron Patterson*
-
-* Deprecate delegating to `arel` in `Relation`.
-
- *Ryuta Kamizono*
-
-* Fix eager loading to respect `store_full_sti_class` setting.
-
- *Ryuta Kamizono*
-
-* Query cache was unavailable when entering the `ActiveRecord::Base.cache` block
- without being connected.
-
- *Tsukasa Oishi*
-
-* Previously, when building records using a `has_many :through` association,
- if the child records were deleted before the parent was saved, they would
- still be persisted. Now, if child records are deleted before the parent is saved
- on a `has_many :through` association, the child records will not be persisted.
-
- *Tobias Kraze*
-
-* Merging two relations representing nested joins no longer transforms the joins of
- the merged relation into LEFT OUTER JOIN. Example to clarify:
-
- ```
- Author.joins(:posts).merge(Post.joins(:comments))
- # Before the change:
- #=> SELECT ... FROM authors INNER JOIN posts ON ... LEFT OUTER JOIN comments ON...
-
- # After the change:
- #=> SELECT ... FROM authors INNER JOIN posts ON ... INNER JOIN comments ON...
- ```
-
- TODO: Add to the Rails 5.2 upgrade guide
-
- *Maxime Handfield Lapointe*
-
-* `ActiveRecord::Persistence#touch` does not work well when optimistic locking enabled and
- `locking_column`, without default value, is null in the database.
-
- *bogdanvlviv*
-
-* Fix destroying existing object does not work well when optimistic locking enabled and
- `locking_column` is null in the database.
-
- *bogdanvlviv*
-
-* Use bulk INSERT to insert fixtures for better performance.
-
- *Kir Shatrov*
-
-* Prevent creation of bind param if casted value is nil.
-
- *Ryuta Kamizono*
-
-* Deprecate passing arguments and block at the same time to `count` and `sum` in `ActiveRecord::Calculations`.
-
- *Ryuta Kamizono*
-
-* Loading model schema from database is now thread-safe.
-
- Fixes #28589.
-
- *Vikrant Chaudhary*, *David Abdemoulaie*
-
-* Add `ActiveRecord::Base#cache_version` to support recyclable cache keys via the new versioned entries
- in `ActiveSupport::Cache`. This also means that `ActiveRecord::Base#cache_key` will now return a stable key
- that does not include a timestamp any more.
-
- NOTE: This feature is turned off by default, and `#cache_key` will still return cache keys with timestamps
- until you set `ActiveRecord::Base.cache_versioning = true`. That's the setting for all new apps on Rails 5.2+
+* Add ActiveRecord::Base.create_or_find_by/! to deal with the SELECT/INSERT race condition in
+ ActiveRecord::Base.find_or_create_by/! by leaning on unique constraints in the database.
*DHH*
-* Respect `SchemaDumper.ignore_tables` in rake tasks for databases structure dump
-
- *Rusty Geldmacher*, *Guillermo Iguaran*
-
-* Add type caster to `RuntimeReflection#alias_name`
-
- Fixes #28959.
-
- *Jon Moss*
-
-* Deprecate `supports_statement_cache?`.
-
- *Ryuta Kamizono*
-
-* Quote database name in `db:create` grant statement (when database user does not have access to create the database).
-
- *Rune Philosof*
-
-* Raise error `UnknownMigrationVersionError` on the movement of migrations
- when the current migration does not exist.
+* Add `Relation#pick` as short-hand for single-value plucks.
- *bogdanvlviv*
-
-* Fix `bin/rails db:forward` first migration.
-
- *bogdanvlviv*
-
-* Support Descending Indexes for MySQL.
-
- MySQL 8.0.1 and higher supports descending indexes: `DESC` in an index definition is no longer ignored.
- See https://dev.mysql.com/doc/refman/8.0/en/descending-indexes.html.
-
- *Ryuta Kamizono*
-
-* Fix inconsistency with changed attributes when overriding AR attribute reader.
-
- *bogdanvlviv*
-
-* When calling the dynamic fixture accessor method with no arguments, it now returns all fixtures of this type.
- Previously this method always returned an empty array.
-
- *Kevin McPhillips*
+ *DHH*
-Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activerecord/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activerecord/CHANGELOG.md) for previous changes.
diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE
index f9e4444f07..04ba107c48 100644
--- a/activerecord/MIT-LICENSE
+++ b/activerecord/MIT-LICENSE
@@ -1,4 +1,6 @@
-Copyright (c) 2004-2017 David Heinemeier Hansson
+Copyright (c) 2004-2018 David Heinemeier Hansson
+
+Arel originally copyright (c) 2007-2016 Nick Kallen, Bryan Helmkamp, Emilio Tagua, Aaron Patterson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc
index ae53ecd177..19650b82ae 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -26,7 +26,7 @@ The Product class is automatically mapped to the table named "products",
which might look like this:
CREATE TABLE products (
- id int NOT NULL auto_increment,
+ id bigint NOT NULL auto_increment,
name varchar(255),
PRIMARY KEY (id)
);
@@ -208,7 +208,7 @@ API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 57c82bf469..170c95b827 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -41,9 +41,11 @@ namespace :test do
end
end
-desc "Build MySQL and PostgreSQL test databases"
namespace :db do
+ desc "Build MySQL and PostgreSQL test databases"
task create: ["db:mysql:build", "db:postgresql:build"]
+
+ desc "Drop MySQL and PostgreSQL test databases"
task drop: ["db:mysql:drop", "db:postgresql:drop"]
end
@@ -68,7 +70,7 @@ end
(Dir["test/cases/**/*_test.rb"].reject {
|x| x.include?("/adapters/")
} + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
- sh(Gem.ruby, "-w" , "-Itest", file)
+ sh(Gem.ruby, "-w", "-Itest", file)
end || raise("Failures")
end
end
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index 7ad06fe840..a857d00c05 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Object-relational mapper framework (part of Rails)."
s.description = "Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
@@ -30,6 +30,4 @@ Gem::Specification.new do |s|
s.add_dependency "activesupport", version
s.add_dependency "activemodel", version
-
- s.add_dependency "arel", "9.0.0.alpha"
end
diff --git a/activerecord/bin/test b/activerecord/bin/test
index 83c192531e..9ecf27ce67 100755
--- a/activerecord/bin/test
+++ b/activerecord/bin/test
@@ -1,6 +1,12 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
+adapter_index = ARGV.index("--adapter") || ARGV.index("-a")
+if adapter_index
+ ARGV.delete_at(adapter_index)
+ ENV["ARCONN"] = ARGV.delete_at(adapter_index).strip
+end
+
COMPONENT_ROOT = File.expand_path("..", __dir__)
require_relative "../../tools/test"
@@ -17,4 +23,5 @@ module Minitest
end
end
+Minitest.load_plugins
Minitest.extensions.unshift "active_record"
diff --git a/activerecord/examples/.gitignore b/activerecord/examples/.gitignore
deleted file mode 100644
index 0dfc1cb7fb..0000000000
--- a/activerecord/examples/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-performance.sql
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 0c19fed9e1..d198466dbf 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2004-2017 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -27,19 +27,20 @@ require "active_support"
require "active_support/rails"
require "active_model"
require "arel"
+require "yaml"
-require_relative "active_record/version"
-require_relative "active_record/attribute_set"
+require "active_record/version"
+require "active_model/attribute_set"
module ActiveRecord
extend ActiveSupport::Autoload
- autoload :Attribute
autoload :Base
autoload :Callbacks
autoload :Core
autoload :ConnectionHandling
autoload :CounterCache
+ autoload :DatabaseConfigurations
autoload :DynamicMatchers
autoload :Enum
autoload :InternalMetadata
@@ -104,6 +105,7 @@ module ActiveRecord
autoload :Result
autoload :TableMetadata
+ autoload :Type
end
module Coders
@@ -162,6 +164,7 @@ module ActiveRecord
"active_record/tasks/postgresql_database_tasks"
end
+ autoload :TestDatabases, "active_record/test_databases"
autoload :TestFixtures, "active_record/fixtures"
def self.eager_load!
@@ -181,3 +184,7 @@ end
ActiveSupport.on_load(:i18n) do
I18n.load_path << File.expand_path("active_record/locale/en.yml", __dir__)
end
+
+YAML.load_tags["!ruby/object:ActiveRecord::AttributeSet"] = "ActiveModel::AttributeSet"
+YAML.load_tags["!ruby/object:ActiveRecord::Attribute::FromDatabase"] = "ActiveModel::Attribute::FromDatabase"
+YAML.load_tags["!ruby/object:ActiveRecord::LazyAttributeHash"] = "ActiveModel::LazyAttributeHash"
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index e5e89734d2..27a641f05b 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -35,7 +35,7 @@ module ActiveRecord
# the database).
#
# class Customer < ActiveRecord::Base
- # composed_of :balance, class_name: "Money", mapping: %w(amount currency)
+ # composed_of :balance, class_name: "Money", mapping: %w(balance amount)
# composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
# end
#
@@ -177,9 +177,9 @@ module ActiveRecord
#
# Once a #composed_of relationship is specified for a model, records can be loaded from the database
# by specifying an instance of the value object in the conditions hash. The following example
- # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
+ # finds all customers with +address_street+ equal to "May Street" and +address_city+ equal to "Chicago":
#
- # Customer.where(balance: Money.new(20, "USD"))
+ # Customer.where(address: Address.new("May Street", "Chicago"))
#
module ClassMethods
# Adds reader and writer methods for manipulating a value object:
@@ -212,8 +212,7 @@ module ActiveRecord
#
# Option examples:
# composed_of :temperature, mapping: %w(reading celsius)
- # composed_of :balance, class_name: "Money", mapping: %w(balance amount),
- # converter: Proc.new { |balance| balance.to_money }
+ # composed_of :balance, class_name: "Money", mapping: %w(balance amount)
# composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
# composed_of :gps_location
# composed_of :gps_location, allow_nil: true
diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb
index 2b0b2864bc..403667fb70 100644
--- a/activerecord/lib/active_record/association_relation.rb
+++ b/activerecord/lib/active_record/association_relation.rb
@@ -2,8 +2,8 @@
module ActiveRecord
class AssociationRelation < Relation
- def initialize(klass, table, predicate_builder, association)
- super(klass, table, predicate_builder)
+ def initialize(klass, association)
+ super(klass)
@association = association
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index ef26f4a20c..0e68e49182 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -3,7 +3,7 @@
require "active_support/core_ext/enumerable"
require "active_support/core_ext/string/conversions"
require "active_support/core_ext/module/remove_method"
-require_relative "errors"
+require "active_record/errors"
module ActiveRecord
class AssociationNotFoundError < ConfigurationError #:nodoc:
@@ -140,26 +140,6 @@ module ActiveRecord
class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
end
- class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner = nil, reflection = nil)
- if owner && reflection
- super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
- else
- super("Cannot associate new records.")
- end
- end
- end
-
- class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner = nil, reflection = nil)
- if owner && reflection
- super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
- else
- super("Cannot dissociate new records.")
- end
- end
- end
-
class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
def initialize(owner = nil, reflection = nil)
if owner && reflection
@@ -189,16 +169,6 @@ module ActiveRecord
end
end
- class ReadOnlyAssociation < ActiveRecordError #:nodoc:
- def initialize(reflection = nil)
- if reflection
- super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
- else
- super("Read-only reflection error.")
- end
- end
- end
-
# This error is raised when trying to destroy a parent instance in N:1 or 1:1 associations
# (has_many, has_one) when there is at least 1 child associated instance.
# ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
@@ -271,7 +241,7 @@ module ActiveRecord
association
end
- def association_cached?(name) # :nodoc
+ def association_cached?(name) # :nodoc:
@association_cache.key?(name)
end
@@ -483,14 +453,14 @@ module ActiveRecord
# The tables for these classes could look something like:
#
# CREATE TABLE users (
- # id int NOT NULL auto_increment,
- # account_id int default NULL,
+ # id bigint NOT NULL auto_increment,
+ # account_id bigint default NULL,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
#
# CREATE TABLE accounts (
- # id int NOT NULL auto_increment,
+ # id bigint NOT NULL auto_increment,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
@@ -557,9 +527,8 @@ module ActiveRecord
# has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event'
# end
#
- # Note: Joining, eager loading and preloading of these associations is not fully possible.
+ # Note: Joining, eager loading and preloading of these associations is not possible.
# These operations happen before instance creation and the scope will be called with a +nil+ argument.
- # This can lead to unexpected behavior and is deprecated.
#
# == Association callbacks
#
@@ -850,7 +819,7 @@ module ActiveRecord
# project.milestones # fetches milestones from the database
# project.milestones.size # uses the milestone cache
# project.milestones.empty? # uses the milestone cache
- # project.milestones(true).size # fetches milestones from the database
+ # project.milestones.reload.size # fetches milestones from the database
# project.milestones # uses the milestone cache
#
# == Eager loading of associations
@@ -1092,12 +1061,6 @@ module ActiveRecord
# belongs_to :dungeon, inverse_of: :evil_wizard
# end
#
- # There are limitations to <tt>:inverse_of</tt> support:
- #
- # * does not work with <tt>:through</tt> associations.
- # * does not work with <tt>:polymorphic</tt> associations.
- # * inverse associations for #belongs_to associations #has_many are ignored.
- #
# For more information, see the documentation for the +:inverse_of+ option.
#
# == Deleting from associations
@@ -1310,6 +1273,9 @@ module ActiveRecord
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_many
# association will use "person_id" as the default <tt>:foreign_key</tt>.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
# [:foreign_type]
# Specify the column used to store the associated object's type, if this is a polymorphic
# association. By default this is guessed to be the name of the polymorphic association
@@ -1383,8 +1349,7 @@ module ActiveRecord
# <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
# Specifies the name of the #belongs_to association on the associated object
- # that is the inverse of this #has_many association. Does not work in combination
- # with <tt>:through</tt> or <tt>:as</tt> options.
+ # that is the inverse of this #has_many association.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:extend]
# Specifies a module or array of modules that will be extended into the association object returned.
@@ -1480,6 +1445,9 @@ module ActiveRecord
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_one association
# will use "person_id" as the default <tt>:foreign_key</tt>.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
# [:foreign_type]
# Specify the column used to store the associated object's type, if this is a polymorphic
# association. By default this is guessed to be the name of the polymorphic association
@@ -1495,6 +1463,9 @@ module ActiveRecord
# <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
# source reflection. You can only use a <tt>:through</tt> query through a #has_one
# or #belongs_to association on the join model.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
# [:source]
# Specifies the source association name used by #has_one <tt>:through</tt> queries.
# Only use it if the name cannot be inferred from the association.
@@ -1515,8 +1486,7 @@ module ActiveRecord
# <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
# Specifies the name of the #belongs_to association on the associated object
- # that is the inverse of this #has_one association. Does not work in combination
- # with <tt>:through</tt> or <tt>:as</tt> options.
+ # that is the inverse of this #has_one association.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:required]
# When set to +true+, the association will also have its presence validated.
@@ -1601,6 +1571,9 @@ module ActiveRecord
# association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
# <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key
# of "favorite_person_id".
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
# [:foreign_type]
# Specify the column used to store the associated object's type, if this is a polymorphic
# association. By default this is guessed to be the name of the association with a "_type"
@@ -1650,8 +1623,7 @@ module ActiveRecord
# +after_commit+ and +after_rollback+ callbacks are executed.
# [:inverse_of]
# Specifies the name of the #has_one or #has_many association on the associated
- # object that is the inverse of this #belongs_to association. Does not work in
- # combination with the <tt>:polymorphic</tt> options.
+ # object that is the inverse of this #belongs_to association.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:optional]
# When set to +true+, the association will not have its presence validated.
@@ -1820,6 +1792,9 @@ module ActiveRecord
# of this class in lower-case and "_id" suffixed. So a Person class that makes
# a #has_and_belongs_to_many association to Project will use "person_id" as the
# default <tt>:foreign_key</tt>.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
# [:association_foreign_key]
# Specify the foreign key used for the association on the receiving side of the association.
# By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed.
@@ -1848,7 +1823,7 @@ module ActiveRecord
builder = Builder::HasAndBelongsToMany.new name, self, options
- join_model = ActiveSupport::Deprecation.silence { builder.through_model }
+ join_model = builder.through_model
const_set join_model.name, join_model
private_constant join_model.name
@@ -1877,7 +1852,7 @@ module ActiveRecord
hm_options[k] = options[k] if options.key? k
end
- ActiveSupport::Deprecation.silence { has_many name, scope, hm_options, &extension }
+ has_many name, scope, hm_options, &extension
_reflections[name.to_s].parent_reflection = habtm_reflection
end
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 096f016976..4f3893588e 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -6,46 +6,36 @@ module ActiveRecord
module Associations
# Keeps track of table aliases for ActiveRecord::Associations::JoinDependency
class AliasTracker # :nodoc:
- def self.create(connection, initial_table)
- aliases = Hash.new(0)
- aliases[initial_table] = 1
- new(connection, aliases)
- end
-
- def self.create_with_joins(connection, initial_table, joins)
+ def self.create(connection, initial_table, joins)
if joins.empty?
- create(connection, initial_table)
+ aliases = Hash.new(0)
else
aliases = Hash.new { |h, k|
h[k] = initial_count_for(connection, k, joins)
}
- aliases[initial_table] = 1
- new(connection, aliases)
end
+ aliases[initial_table] = 1
+ new(connection, aliases)
end
def self.initial_count_for(connection, name, table_joins)
- # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
- quoted_name = connection.quote_table_name(name).downcase
+ quoted_name = nil
counts = table_joins.map do |join|
if join.is_a?(Arel::Nodes::StringJoin)
+ # quoted_name should be case ignored as some database adapters (Oracle) return quoted name in uppercase
+ quoted_name ||= connection.quote_table_name(name)
+
# Table names + table aliases
- join.left.downcase.scan(
- /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
+ join.left.scan(
+ /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i
).size
- elsif join.respond_to? :left
- join.left.table_name == name ? 1 : 0
+ elsif join.is_a?(Arel::Nodes::Join)
+ join.left.name == name ? 1 : 0
+ elsif join.is_a?(Hash)
+ join.fetch(name, 0)
else
- # this branch is reached by two tests:
- #
- # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37
- # with :posts
- #
- # activerecord/test/cases/associations/eager_test.rb:1133
- # with :comments
- #
- 0
+ raise ArgumentError, "joins list should be initialized by list of Arel::Nodes::Join"
end
end
@@ -79,10 +69,7 @@ module ActiveRecord
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
- attr_reader :aliases
+ attr_reader :aliases
private
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index ca1f9f1650..ca8c7794e0 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -124,7 +124,7 @@ module ActiveRecord
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
# through association's scope)
def target_scope
- AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all)
+ AssociationRelation.create(klass, self).merge!(klass.all)
end
def extensions
@@ -156,9 +156,9 @@ module ActiveRecord
reset
end
- # We can't dump @reflection since it contains the scope proc
+ # We can't dump @reflection and @through_reflection since it contains the scope proc
def marshal_dump
- ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] }
+ ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] }
[@reflection.name, ivars]
end
@@ -201,8 +201,8 @@ module ActiveRecord
if (reflection.has_one? || reflection.collection?) && !options[:through]
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
- if reflection.options[:as]
- attributes[reflection.type] = owner.class.base_class.name
+ if reflection.type
+ attributes[reflection.type] = owner.class.polymorphic_name
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index b2dc044312..0a90a6104a 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -23,8 +23,7 @@ module ActiveRecord
reflection = association.reflection
scope = klass.unscoped
owner = association.owner
- alias_tracker = AliasTracker.create(klass.connection, klass.table_name)
- chain = get_chain(reflection, association, alias_tracker)
+ chain = get_chain(reflection, association, scope.alias_tracker)
scope.extending! reflection.extensions
add_constraints(scope, owner, chain)
@@ -36,24 +35,20 @@ module ActiveRecord
binds << last_reflection.join_id_for(owner)
if last_reflection.type
- binds << owner.class.base_class.name
+ binds << owner.class.polymorphic_name
end
chain.each_cons(2).each do |reflection, next_reflection|
if reflection.type
- binds << next_reflection.klass.base_class.name
+ binds << next_reflection.klass.polymorphic_name
end
end
binds
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :value_transformation
- private
def join(table, constraint)
table.create_join(table, table.create_on(constraint))
end
@@ -68,7 +63,7 @@ module ActiveRecord
scope = apply_scope(scope, table, key, value)
if reflection.type
- polymorphic_type = transform_value(owner.class.base_class.name)
+ polymorphic_type = transform_value(owner.class.polymorphic_name)
scope = apply_scope(scope, table, reflection.type, polymorphic_type)
end
@@ -89,7 +84,7 @@ module ActiveRecord
constraint = table[key].eq(foreign_table[foreign_key])
if reflection.type
- value = transform_value(next_reflection.klass.base_class.name)
+ value = transform_value(next_reflection.klass.polymorphic_name)
scope = apply_scope(scope, table, reflection.type, value)
end
@@ -136,7 +131,7 @@ module ActiveRecord
item = eval_scope(reflection, scope_chain_item, owner)
if scope_chain_item == chain_head.scope
- scope.merge! item.except(:where, :includes)
+ scope.merge! item.except(:where, :includes, :unscope, :order)
end
reflection.all_includes do
@@ -145,7 +140,7 @@ module ActiveRecord
scope.unscope!(*item.unscope_values)
scope.where_clause += item.where_clause
- scope.order_values |= item.order_values
+ scope.order_values = item.order_values | scope.order_values
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index ba54cd8f49..1109fee462 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -5,24 +5,35 @@ module ActiveRecord
# = Active Record Belongs To Association
class BelongsToAssociation < SingularAssociation #:nodoc:
def handle_dependency
- target.send(options[:dependent]) if load_target
+ return unless load_target
+
+ case options[:dependent]
+ when :destroy
+ target.destroy
+ raise ActiveRecord::Rollback unless target.destroyed?
+ else
+ target.send(options[:dependent])
+ end
end
def replace(record)
if record
raise_on_type_mismatch!(record)
update_counters_on_replace(record)
- replace_keys(record)
set_inverse_instance(record)
@updated = true
else
decrement_counters
- remove_keys
end
self.target = record
end
+ def target=(record)
+ replace_keys(record)
+ super
+ end
+
def default(&block)
writer(owner.instance_exec(&block)) if reader.nil?
end
@@ -49,9 +60,9 @@ module ActiveRecord
def update_counters(by)
if require_counter_update? && foreign_key_present?
if target && !stale_target?
- target.increment!(reflection.counter_cache_column, by)
+ target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch])
else
- klass.update_counters(target_id, reflection.counter_cache_column => by)
+ klass.update_counters(target_id, reflection.counter_cache_column => by, touch: reflection.options[:touch])
end
end
end
@@ -78,11 +89,8 @@ module ActiveRecord
end
def replace_keys(record)
- owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class))
- end
-
- def remove_keys
- owner[reflection.foreign_key] = nil
+ owner[reflection.foreign_key] = record ?
+ record._read_attribute(reflection.association_primary_key(record.class)) : nil
end
def foreign_key_present?
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index 4ce3474bd5..75b4c4481a 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -13,12 +13,7 @@ module ActiveRecord
def replace_keys(record)
super
- owner[reflection.foreign_type] = record.class.base_class.name
- end
-
- def remove_keys
- super
- owner[reflection.foreign_type] = nil
+ owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil
end
def different_target?(record)
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index ca3032d967..7c69cd65ee 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -104,8 +104,8 @@ module ActiveRecord::Associations::Builder # :nodoc:
def self.define_readers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}(*args)
- association(:#{name}).reader(*args)
+ def #{name}
+ association(:#{name}).reader
end
CODE
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 9904ee4bed..c161454c1a 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -114,9 +114,13 @@ module ActiveRecord::Associations::Builder # :nodoc:
BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method)
}}
- model.after_save callback.(:saved_changes), if: :saved_changes?
- model.after_touch callback.(:changes_to_save)
- model.after_destroy callback.(:changes_to_save)
+ unless reflection.counter_cache_column
+ model.after_create callback.(:saved_changes), if: :saved_changes?
+ model.after_destroy callback.(:changes_to_save)
+ end
+
+ model.after_update callback.(:saved_changes), if: :saved_changes?
+ model.after_touch callback.(:changes_to_save)
end
def self.add_default_callbacks(model, reflection)
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index 753fde5146..35a72c3850 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../associations"
+require "active_record/associations"
module ActiveRecord::Associations::Builder # :nodoc:
class CollectionAssociation < Association #:nodoc:
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 12fcfbcd45..1981da11a2 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
@@ -47,7 +47,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
habtm = JoinTableResolver.build lhs_model, association_name, options
join_model = Class.new(ActiveRecord::Base) {
- class << self;
+ class << self
attr_accessor :left_model
attr_accessor :name
attr_accessor :table_name_resolver
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index ceedf150e3..d61d105544 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -45,6 +45,8 @@ module ActiveRecord
def ids_reader
if loaded?
target.pluck(reflection.association_primary_key)
+ elsif !target.empty?
+ load_target.pluck(reflection.association_primary_key)
else
@association_ids ||= scope.pluck(reflection.association_primary_key)
end
@@ -52,11 +54,11 @@ module ActiveRecord
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
def ids_writer(ids)
- pk_type = reflection.association_primary_key_type
+ primary_key = reflection.association_primary_key
+ pk_type = klass.type_for_attribute(primary_key)
ids = Array(ids).reject(&:blank?)
ids.map! { |i| pk_type.cast(i) }
- primary_key = reflection.association_primary_key
records = klass.where(primary_key => ids).index_by do |r|
r.public_send(primary_key)
end.values_at(*ids).compact
@@ -79,7 +81,13 @@ module ActiveRecord
def find(*args)
if options[:inverse_of] && loaded?
args_flatten = args.flatten
- raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank?
+ model = scope.klass
+
+ if args_flatten.blank?
+ error_message = "Couldn't find #{model.name} without an ID"
+ raise RecordNotFound.new(error_message, model.name, model.primary_key, args)
+ end
+
result = find_by_scan(*args)
result_size = Array(result).size
@@ -181,8 +189,6 @@ module ActiveRecord
# are actually removed from the database, that depends precisely on
# +delete_records+. They are in any case removed from the collection.
def delete(*records)
- return if records.empty?
- records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) }
delete_or_destroy(records, options[:dependent])
end
@@ -192,8 +198,6 @@ module ActiveRecord
# Note that this method removes records from the database ignoring the
# +:dependent+ option.
def destroy(*records)
- return if records.empty?
- records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) }
delete_or_destroy(records, :destroy)
end
@@ -210,9 +214,11 @@ module ActiveRecord
def size
if !find_target? || loaded?
target.size
+ elsif @association_ids
+ @association_ids.size
elsif !association_scope.group_values.empty?
load_target.size
- elsif !association_scope.distinct_value && target.is_a?(Array)
+ elsif !association_scope.distinct_value && !target.empty?
unsaved_records = target.select(&:new_record?)
unsaved_records.size + count_records
else
@@ -229,10 +235,10 @@ module ActiveRecord
# loaded and you are going to fetch the records anyway it is better to
# check <tt>collection.length.zero?</tt>.
def empty?
- if loaded?
+ if loaded? || @association_ids
size.zero?
else
- @target.blank? && !scope.exists?
+ target.empty? && !scope.exists?
end
end
@@ -376,6 +382,8 @@ module ActiveRecord
end
def delete_or_destroy(records, method)
+ return if records.empty?
+ records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) }
records = records.flatten
records.each { |record| raise_on_type_mismatch!(record) }
existing_records = records.reject(&:new_record?)
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 412e89255d..9a30198b95 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -32,7 +32,7 @@ module ActiveRecord
class CollectionProxy < Relation
def initialize(klass, association) #:nodoc:
@association = association
- super klass, klass.arel_table, klass.predicate_builder
+ super klass
extensions = association.extensions
extend(*extensions) if extensions.any?
@@ -988,6 +988,12 @@ module ActiveRecord
load_target == other
end
+ ##
+ # :method: to_ary
+ #
+ # :call-seq:
+ # to_ary()
+ #
# Returns a new array of objects from the collection. If the collection
# hasn't been loaded, it fetches the records from the database.
#
@@ -1021,10 +1027,6 @@ module ActiveRecord
# # #<Pet id: 5, name: "Brain", person_id: 1>,
# # #<Pet id: 6, name: "Boss", person_id: 1>
# # ]
- def to_ary
- load_target.dup
- end
- alias_method :to_a, :to_ary
def records # :nodoc:
load_target
@@ -1072,7 +1074,6 @@ module ActiveRecord
end
# Reloads the collection from the database. Returns +self+.
- # Equivalent to <tt>collection(true)</tt>.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -1086,9 +1087,6 @@ module ActiveRecord
#
# person.pets.reload # fetches pets from the database
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
- #
- # person.pets(true) # fetches pets from the database
- # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
def reload
proxy_association.reload
reset_scope
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 07c7f28d2d..cf85a87fa7 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -63,7 +63,7 @@ module ActiveRecord
count = if reflection.has_cached_counter?
owner._read_attribute(reflection.counter_cache_column).to_i
else
- scope.count
+ scope.count(:all)
end
# If there's nothing in the database and @target has no new records
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index adbf52b87c..59929b8c4e 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -8,9 +8,7 @@ module ActiveRecord
def initialize(owner, reflection)
super
-
- @through_records = {}
- @through_association = nil
+ @through_records = {}
end
def concat(*records)
@@ -50,11 +48,6 @@ module ActiveRecord
end
private
-
- def through_association
- @through_association ||= owner.association(through_reflection.name)
- end
-
# The through record (built with build_record) is temporarily cached
# so that it may be reused if insert_record is subsequently called.
#
@@ -140,21 +133,15 @@ module ActiveRecord
scope = through_association.scope
scope.where! construct_join_attributes(*records)
+ scope = scope.where(through_scope_attributes)
case method
when :destroy
if scope.klass.primary_key
- count = scope.destroy_all.length
+ count = scope.destroy_all.count(&:destroyed?)
else
scope.each(&:_run_destroy_callbacks)
-
- arel = scope.arel
-
- stmt = Arel::DeleteManager.new
- stmt.from scope.klass.arel_table
- stmt.wheres = arel.constraints
-
- count = scope.klass.connection.delete(stmt, "SQL")
+ count = scope.delete_all
end
when :nullify
count = scope.update_all(source_reflection.foreign_key => nil)
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 7953b89f61..090b082cb0 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -60,6 +60,7 @@ module ActiveRecord
when :destroy
target.destroyed_by_association = reflection
target.destroy
+ throw(:abort) unless target.destroyed?
when :nullify
target.update_columns(reflection.foreign_key => nil) if target.persisted?
end
diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb
index 36746f9115..019bf0729f 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -6,17 +6,16 @@ module ActiveRecord
class HasOneThroughAssociation < HasOneAssociation #:nodoc:
include ThroughAssociation
- def replace(record)
- create_through_record(record)
+ def replace(record, save = true)
+ create_through_record(record, save)
self.target = record
end
private
-
- def create_through_record(record)
+ def create_through_record(record, save)
ensure_not_nested
- through_proxy = owner.association(through_reflection.name)
+ through_proxy = through_association
through_record = through_proxy.load_target
if through_record && !record
@@ -29,8 +28,12 @@ module ActiveRecord
end
if through_record
- through_record.update(attributes)
- elsif owner.new_record?
+ if through_record.new_record?
+ through_record.assign_attributes(attributes)
+ else
+ through_record.update(attributes)
+ end
+ elsif owner.new_record? || !save
through_proxy.build(attributes)
else
through_proxy.create(attributes)
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 77b3d11b47..f88e383fe0 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -43,8 +43,6 @@ module ActiveRecord
Column = Struct.new(:name, :alias)
end
- attr_reader :alias_tracker, :base_klass, :join_root
-
def self.make_tree(associations)
hash = {}
walk_tree associations, hash
@@ -69,30 +67,8 @@ module ActiveRecord
end
end
- # base is the base class on which operation is taking place.
- # associations is the list of associations which are joined using hash, symbol or array.
- # joins is the list of all string join commands and arel nodes.
- #
- # Example :
- #
- # class Physician < ActiveRecord::Base
- # has_many :appointments
- # has_many :patients, through: :appointments
- # end
- #
- # If I execute `@physician.patients.to_a` then
- # base # => Physician
- # associations # => []
- # joins # => [#<Arel::Nodes::InnerJoin: ...]
- #
- # However if I execute `Physician.joins(:appointments).to_a` then
- # base # => Physician
- # associations # => [:appointments]
- # joins # => []
- #
- def initialize(base, table, associations, joins, eager_loading: true)
- @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins)
- @eager_loading = eager_loading
+ def initialize(base, table, associations, alias_tracker)
+ @alias_tracker = alias_tracker
tree = self.class.make_tree associations
@join_root = JoinBase.new(base, table, build(tree, base))
@join_root.children.each { |child| construct_tables! @join_root, child }
@@ -158,6 +134,9 @@ module ActiveRecord
parents.values
end
+ protected
+ attr_reader :alias_tracker, :base_klass, :join_root
+
private
def make_constraints(parent, child, tables, join_type)
@@ -220,12 +199,11 @@ module ActiveRecord
reflection.check_eager_loadable!
if reflection.polymorphic?
- next unless @eager_loading
raise EagerLoadPolymorphicError.new(reflection)
end
- JoinAssociation.new reflection, build(right, reflection.klass)
- end.compact
+ JoinAssociation.new(reflection, build(right, reflection.klass), alias_tracker)
+ end
end
def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index a526468bf6..c36386ec7e 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "join_part"
+require "active_record/associations/join_dependency/join_part"
module ActiveRecord
module Associations
@@ -11,11 +11,12 @@ module ActiveRecord
attr_accessor :tables
- def initialize(reflection, children)
+ def initialize(reflection, children, alias_tracker)
super(reflection.klass, children)
- @reflection = reflection
- @tables = nil
+ @alias_tracker = alias_tracker
+ @reflection = reflection
+ @tables = nil
end
def match?(other)
@@ -38,11 +39,12 @@ module ActiveRecord
joins << table.create_join(table, table.create_on(constraint), join_type)
join_scope = reflection.join_scope(table, foreign_klass)
+ arel = join_scope.arel(alias_tracker.aliases)
- if join_scope.arel.constraints.any?
- joins.concat join_scope.arel.join_sources
+ if arel.constraints.any?
+ joins.concat arel.join_sources
right = joins.last.right
- right.expr = right.expr.and(join_scope.arel.constraints)
+ right.expr = right.expr.and(arel.constraints)
end
# The current table in this iteration becomes the foreign table in the next
@@ -55,6 +57,9 @@ module ActiveRecord
def table
tables.first
end
+
+ private
+ attr_reader :alias_tracker
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
index 8a8fa8993b..988b4e8fa2 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "join_part"
+require "active_record/associations/join_dependency/join_part"
module ActiveRecord
module Associations
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index e1754d4a19..5c2ac5b374 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -44,16 +44,8 @@ module ActiveRecord
extend ActiveSupport::Autoload
eager_autoload do
- autoload :Association, "active_record/associations/preloader/association"
- autoload :SingularAssociation, "active_record/associations/preloader/singular_association"
- autoload :CollectionAssociation, "active_record/associations/preloader/collection_association"
- autoload :ThroughAssociation, "active_record/associations/preloader/through_association"
-
- autoload :HasMany, "active_record/associations/preloader/has_many"
- autoload :HasManyThrough, "active_record/associations/preloader/has_many_through"
- autoload :HasOne, "active_record/associations/preloader/has_one"
- autoload :HasOneThrough, "active_record/associations/preloader/has_one_through"
- autoload :BelongsTo, "active_record/associations/preloader/belongs_to"
+ autoload :Association, "active_record/associations/preloader/association"
+ autoload :ThroughAssociation, "active_record/associations/preloader/through_association"
end
# Eager loads the named associations for the given Active Record record(s).
@@ -91,7 +83,7 @@ module ActiveRecord
# { author: :avatar }
# [ :books, { author: :avatar } ]
def preload(records, associations, preload_scope = nil)
- records = records.compact
+ records = Array.wrap(records).compact
if records.empty?
[]
@@ -106,26 +98,30 @@ module ActiveRecord
private
# Loads all the given data into +records+ for the +association+.
- def preloaders_on(association, records, scope)
+ def preloaders_on(association, records, scope, polymorphic_parent = false)
case association
when Hash
- preloaders_for_hash(association, records, scope)
+ preloaders_for_hash(association, records, scope, polymorphic_parent)
when Symbol
- preloaders_for_one(association, records, scope)
+ preloaders_for_one(association, records, scope, polymorphic_parent)
when String
- preloaders_for_one(association.to_sym, records, scope)
+ preloaders_for_one(association.to_sym, records, scope, polymorphic_parent)
else
raise ArgumentError, "#{association.inspect} was not recognized for preload"
end
end
- def preloaders_for_hash(association, records, scope)
+ def preloaders_for_hash(association, records, scope, polymorphic_parent)
association.flat_map { |parent, child|
- loaders = preloaders_for_one parent, records, scope
+ loaders = preloaders_for_one parent, records, scope, polymorphic_parent
recs = loaders.flat_map(&:preloaded_records).uniq
+
+ reflection = records.first.class._reflect_on_association(parent)
+ polymorphic_parent = reflection && reflection.options[:polymorphic]
+
loaders.concat Array.wrap(child).flat_map { |assoc|
- preloaders_on assoc, recs, scope
+ preloaders_on assoc, recs, scope, polymorphic_parent
}
loaders
}
@@ -143,8 +139,8 @@ module ActiveRecord
# Additionally, polymorphic belongs_to associations can have multiple associated
# classes, depending on the polymorphic_type field. So we group by the classes as
# well.
- def preloaders_for_one(association, records, scope)
- grouped_records(association, records).flat_map do |reflection, klasses|
+ def preloaders_for_one(association, records, scope, polymorphic_parent)
+ grouped_records(association, records, polymorphic_parent).flat_map do |reflection, klasses|
klasses.map do |rhs_klass, rs|
loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
loader.run self
@@ -153,10 +149,11 @@ module ActiveRecord
end
end
- def grouped_records(association, records)
+ def grouped_records(association, records, polymorphic_parent)
h = {}
records.each do |record|
next unless record
+ next if polymorphic_parent && !record.class._reflect_on_association(association)
assoc = record.association(association)
next unless assoc.klass
klasses = h[assoc.reflection] ||= {}
@@ -166,8 +163,6 @@ module ActiveRecord
end
class AlreadyLoaded # :nodoc:
- attr_reader :owners, :reflection
-
def initialize(klass, owners, reflection, preload_scope)
@owners = owners
@reflection = reflection
@@ -178,11 +173,13 @@ module ActiveRecord
def preloaded_records
owners.flat_map { |owner| owner.association(reflection.name).target }
end
+
+ private
+ attr_reader :owners, :reflection
end
# Returns a class containing the logic needed to load preload the data
- # and attach it to a relation. For example +Preloader::Association+ or
- # +Preloader::HasManyThrough+. The class returned implements a `run` method
+ # and attach it to a relation. The class returned implements a `run` method
# that accepts a preloader.
def preloader_for(reflection, owners)
if owners.first.association(reflection.name).loaded?
@@ -190,13 +187,10 @@ module ActiveRecord
end
reflection.check_preloadable!
- case reflection.macro
- when :has_many
- reflection.options[:through] ? HasManyThrough : HasMany
- when :has_one
- reflection.options[:through] ? HasOneThrough : HasOne
- when :belongs_to
- BelongsTo
+ if reflection.options[:through]
+ ThroughAssociation
+ else
+ Association
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index fe696e0d6e..d6f7359055 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -4,7 +4,6 @@ module ActiveRecord
module Associations
class Preloader
class Association #:nodoc:
- attr_reader :owners, :reflection, :preload_scope, :model, :klass
attr_reader :preloaded_records
def initialize(klass, owners, reflection, preload_scope)
@@ -17,12 +16,20 @@ module ActiveRecord
end
def run(preloader)
- associated_records_by_owner(preloader).each do |owner, records|
- associate_records_to_owner(owner, records)
+ records = load_records do |record|
+ owner = owners_by_key[convert_key(record[association_key_name])]
+ association = owner.association(reflection.name)
+ association.set_inverse_instance(record)
+ end
+
+ owners.each do |owner|
+ associate_records_to_owner(owner, records[convert_key(owner[owner_key_name])] || [])
end
end
private
+ attr_reader :owners, :reflection, :preload_scope, :model, :klass
+
# The name of the key on the associated records
def association_key_name
reflection.join_primary_key(klass)
@@ -33,37 +40,25 @@ module ActiveRecord
reflection.join_foreign_key
end
- def associated_records_by_owner(preloader)
- records = load_records do |record|
- owner = owners_by_key[convert_key(record[association_key_name])]
- association = owner.association(reflection.name)
- association.set_inverse_instance(record)
- end
-
- owners.each_with_object({}) do |owner, result|
- result[owner] = records[convert_key(owner[owner_key_name])] || []
- end
- end
-
def associate_records_to_owner(owner, records)
- raise NotImplementedError
+ association = owner.association(reflection.name)
+ association.loaded!
+ if reflection.collection?
+ association.target.concat(records)
+ else
+ association.target = records.first unless records.empty?
+ end
end
def owner_keys
- unless defined?(@owner_keys)
- @owner_keys = owners.map do |owner|
- owner[owner_key_name]
- end
- @owner_keys.uniq!
- @owner_keys.compact!
- end
- @owner_keys
+ @owner_keys ||= owners_by_key.keys
end
def owners_by_key
unless defined?(@owners_by_key)
@owners_by_key = owners.each_with_object({}) do |owner, h|
- h[convert_key(owner[owner_key_name])] = owner
+ key = convert_key(owner[owner_key_name])
+ h[key] = owner if key
end
end
@owners_by_key
@@ -86,11 +81,11 @@ module ActiveRecord
end
def association_key_type
- @klass.type_for_attribute(association_key_name.to_s).type
+ @klass.type_for_attribute(association_key_name).type
end
def owner_key_type
- @model.type_for_attribute(owner_key_name.to_s).type
+ @model.type_for_attribute(owner_key_name).type
end
def load_records(&block)
@@ -122,7 +117,7 @@ module ActiveRecord
scope = klass.scope_for_association
if reflection.type
- scope.where!(reflection.type => model.base_class.sti_name)
+ scope.where!(reflection.type => model.polymorphic_name)
end
scope.merge!(reflection_scope) if reflection.scope
diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb
deleted file mode 100644
index a8e3340b23..0000000000
--- a/activerecord/lib/active_record/associations/preloader/belongs_to.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class BelongsTo < SingularAssociation #:nodoc:
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
deleted file mode 100644
index fc2029f54a..0000000000
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class CollectionAssociation < Association #:nodoc:
- private
- def associate_records_to_owner(owner, records)
- association = owner.association(reflection.name)
- association.loaded!
- association.target.concat(records)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many.rb b/activerecord/lib/active_record/associations/preloader/has_many.rb
deleted file mode 100644
index 72f55bc43f..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_many.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class HasMany < CollectionAssociation #:nodoc:
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
deleted file mode 100644
index 0639fdca44..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class HasManyThrough < CollectionAssociation #:nodoc:
- include ThroughAssociation
-
- def associated_records_by_owner(preloader)
- records_by_owner = super
-
- if reflection_scope.distinct_value
- records_by_owner.each_value(&:uniq!)
- end
-
- records_by_owner
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb
deleted file mode 100644
index e339b65fb5..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_one.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class HasOne < SingularAssociation #:nodoc:
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_one_through.rb b/activerecord/lib/active_record/associations/preloader/has_one_through.rb
deleted file mode 100644
index 17734d0257..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_one_through.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class HasOneThrough < SingularAssociation #:nodoc:
- include ThroughAssociation
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb
deleted file mode 100644
index 30a92411e3..0000000000
--- a/activerecord/lib/active_record/associations/preloader/singular_association.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class SingularAssociation < Association #:nodoc:
- private
- def associate_records_to_owner(owner, records)
- association = owner.association(reflection.name)
- association.target = records.first
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index fa32cc5553..a6b7ab80a2 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -3,78 +3,54 @@
module ActiveRecord
module Associations
class Preloader
- module ThroughAssociation #:nodoc:
- def through_reflection
- reflection.through_reflection
- end
-
- def source_reflection
- reflection.source_reflection
- end
-
- def associated_records_by_owner(preloader)
- through_scope = through_scope()
-
- preloader.preload(owners,
- through_reflection.name,
- through_scope)
-
- through_records = owners.map do |owner|
- center = owner.association(through_reflection.name).target
- [owner, Array(center)]
- end
-
- reset_association(owners, through_reflection.name, through_scope)
-
- middle_records = through_records.flat_map(&:last)
-
- reflection_scope = reflection_scope() if reflection.scope
-
- preloaders = preloader.preload(middle_records,
- source_reflection.name,
- reflection_scope)
-
+ class ThroughAssociation < Association # :nodoc:
+ def run(preloader)
+ already_loaded = owners.first.association(through_reflection.name).loaded?
+ through_scope = through_scope()
+ reflection_scope = target_reflection_scope
+ through_preloaders = preloader.preload(owners, through_reflection.name, through_scope)
+ middle_records = through_preloaders.flat_map(&:preloaded_records)
+ preloaders = preloader.preload(middle_records, source_reflection.name, reflection_scope)
@preloaded_records = preloaders.flat_map(&:preloaded_records)
- middle_to_pl = preloaders.each_with_object({}) do |pl, h|
- pl.owners.each { |middle|
- h[middle] = pl
- }
- end
-
- 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|
- rhs_records = middles.flat_map { |r|
- r.association(source_reflection.name).target
- }.compact
-
- # Respect the order on `reflection_scope` if it exists, else use the natural order.
- if reflection_scope && !reflection_scope.order_values.empty?
- @id_map ||= id_to_index_map @preloaded_records
- rhs_records.sort_by { |rhs| @id_map[rhs] }
- else
- rhs_records
+ owners.each do |owner|
+ through_records = Array(owner.association(through_reflection.name).target)
+ if already_loaded
+ if source_type = reflection.options[:source_type]
+ through_records = through_records.select do |record|
+ record[reflection.foreign_type] == source_type
+ end
end
+ else
+ owner.association(through_reflection.name).reset if through_scope
+ end
+ result = through_records.flat_map do |record|
+ association = record.association(source_reflection.name)
+ target = association.target
+ association.reset if preload_scope
+ target
end
+ result.compact!
+ if reflection_scope
+ result.sort_by! { |rhs| preload_index[rhs] } if reflection_scope.order_values.any?
+ result.uniq! if reflection_scope.distinct_value
+ end
+ associate_records_to_owner(owner, result)
end
end
private
+ def through_reflection
+ reflection.through_reflection
+ end
- def id_to_index_map(ids)
- id_map = {}
- ids.each_with_index { |id, index| id_map[id] = index }
- id_map
+ def source_reflection
+ reflection.source_reflection
end
- def reset_association(owners, association_name, should_reset)
- # Don't cache the association - we would only be caching a subset
- if should_reset
- owners.each { |owner|
- owner.association(association_name).reset
- }
+ def preload_index
+ @preload_index ||= @preloaded_records.each_with_object({}).with_index do |(id, result), index|
+ result[id] = index
end
end
@@ -115,6 +91,16 @@ module ActiveRecord
scope unless scope.empty_scope?
end
+
+ def target_reflection_scope
+ if preload_scope
+ reflection_scope.merge(preload_scope)
+ elsif reflection.scope
+ reflection_scope
+ else
+ nil
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index bce2a95ce1..5afb0bc068 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -4,9 +4,24 @@ module ActiveRecord
module Associations
# = Active Record Through Association
module ThroughAssociation #:nodoc:
- delegate :source_reflection, :through_reflection, to: :reflection
+ delegate :source_reflection, to: :reflection
private
+ def through_reflection
+ @through_reflection ||= begin
+ refl = reflection.through_reflection
+
+ while refl.through_reflection?
+ refl = refl.through_reflection
+ end
+
+ refl
+ end
+ end
+
+ def through_association
+ @through_association ||= owner.association(through_reflection.name)
+ end
# We merge in these scopes for two reasons:
#
@@ -38,24 +53,22 @@ module ActiveRecord
def construct_join_attributes(*records)
ensure_mutable
- if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key
+ association_primary_key = source_reflection.association_primary_key(reflection.klass)
+
+ if association_primary_key == reflection.klass.primary_key && !options[:source_type]
join_attributes = { source_reflection.name => records }
else
join_attributes = {
- source_reflection.foreign_key =>
- records.map { |record|
- record.send(source_reflection.association_primary_key(reflection.klass))
- }
+ source_reflection.foreign_key => records.map(&association_primary_key.to_sym)
}
end
if options[:source_type]
- join_attributes[source_reflection.foreign_type] =
- records.map { |record| record.class.base_class.name }
+ join_attributes[source_reflection.foreign_type] = [ options[:source_type] ]
end
if records.count == 1
- Hash[join_attributes.map { |k, v| [k, v.first] }]
+ join_attributes.transform_values!(&:first)
else
join_attributes
end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 8b0d9aab01..b6f0e18764 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -4,14 +4,8 @@ require "active_model/forbidden_attributes_protection"
module ActiveRecord
module AttributeAssignment
- extend ActiveSupport::Concern
include ActiveModel::AttributeAssignment
- # Alias for ActiveModel::AttributeAssignment#assign_attributes. See ActiveModel::AttributeAssignment.
- def attributes=(attributes)
- assign_attributes(attributes)
- end
-
private
def _assign_attributes(attributes)
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index e4ca6c8408..83b5a5e698 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -33,7 +33,9 @@ module ActiveRecord
BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
- class GeneratedAttributeMethods < Module; end # :nodoc:
+ class GeneratedAttributeMethods < Module #:nodoc:
+ include Mutex_m
+ end
module ClassMethods
def inherited(child_class) #:nodoc:
@@ -42,7 +44,7 @@ module ActiveRecord
end
def initialize_generated_modules # :nodoc:
- @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m }
+ @generated_attribute_methods = GeneratedAttributeMethods.new
@attribute_methods_generated = false
include @generated_attribute_methods
@@ -57,7 +59,7 @@ module ActiveRecord
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
- superclass.define_attribute_methods unless self == base_class
+ superclass.define_attribute_methods unless base_class?
super(attribute_names)
@attribute_methods_generated = true
end
@@ -165,6 +167,57 @@ module ActiveRecord
end
end
+ # Regexp whitelist. Matches the following:
+ # "#{table_name}.#{column_name}"
+ # "#{column_name}"
+ COLUMN_NAME_WHITELIST = /\A(?:\w+\.)?\w+\z/i
+
+ # Regexp whitelist. Matches the following:
+ # "#{table_name}.#{column_name}"
+ # "#{table_name}.#{column_name} #{direction}"
+ # "#{table_name}.#{column_name} #{direction} NULLS FIRST"
+ # "#{table_name}.#{column_name} NULLS LAST"
+ # "#{column_name}"
+ # "#{column_name} #{direction}"
+ # "#{column_name} #{direction} NULLS FIRST"
+ # "#{column_name} NULLS LAST"
+ COLUMN_NAME_ORDER_WHITELIST = /
+ \A
+ (?:\w+\.)?
+ \w+
+ (?:\s+asc|\s+desc)?
+ (?:\s+nulls\s+(?:first|last))?
+ \z
+ /ix
+
+ def enforce_raw_sql_whitelist(args, whitelist: COLUMN_NAME_WHITELIST) # :nodoc:
+ unexpected = args.reject do |arg|
+ arg.kind_of?(Arel::Node) ||
+ arg.is_a?(Arel::Nodes::SqlLiteral) ||
+ arg.is_a?(Arel::Attributes::Attribute) ||
+ arg.to_s.split(/\s*,\s*/).all? { |part| whitelist.match?(part) }
+ end
+
+ return if unexpected.none?
+
+ if allow_unsafe_raw_sql == :deprecated
+ ActiveSupport::Deprecation.warn(
+ "Dangerous query method (method whose arguments are used as raw " \
+ "SQL) called with non-attribute argument(s): " \
+ "#{unexpected.map(&:inspect).join(", ")}. Non-attribute " \
+ "arguments will be disallowed in Rails 6.0. This method should " \
+ "not be called with user-provided values, such as request " \
+ "parameters or model attributes. Known-safe values can be passed " \
+ "by wrapping them in Arel.sql()."
+ )
+ else
+ raise(ActiveRecord::UnknownAttributeReference,
+ "Query method called with non-attribute argument(s): " +
+ unexpected.map(&:inspect).join(", ")
+ )
+ end
+ end
+
# Returns true if the given attribute exists, otherwise false.
#
# class Person < ActiveRecord::Base
@@ -234,7 +287,7 @@ module ActiveRecord
return has_attribute?(name)
end
- return true
+ true
end
# Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
@@ -390,33 +443,24 @@ module ActiveRecord
@attributes.accessed
end
- protected
-
- def attribute_method?(attr_name) # :nodoc:
+ private
+ def attribute_method?(attr_name)
# We check defined? because Syck calls respond_to? before actually calling initialize.
defined?(@attributes) && @attributes.key?(attr_name)
end
- private
-
- def arel_attributes_with_values_for_create(attribute_names)
- arel_attributes_with_values(attributes_for_create(attribute_names))
+ def attributes_with_values_for_create(attribute_names)
+ attributes_with_values(attributes_for_create(attribute_names))
end
- def arel_attributes_with_values_for_update(attribute_names)
- arel_attributes_with_values(attributes_for_update(attribute_names))
+ def attributes_with_values_for_update(attribute_names)
+ attributes_with_values(attributes_for_update(attribute_names))
end
- # Returns a Hash of the Arel::Attributes and attribute values that have been
- # typecasted for use in an Arel insert/update method.
- def arel_attributes_with_values(attribute_names)
- attrs = {}
- arel_table = self.class.arel_table
-
- attribute_names.each do |name|
- attrs[arel_table[name]] = typecasted_attribute_value(name)
+ def attributes_with_values(attribute_names)
+ attribute_names.each_with_object({}) do |name, attrs|
+ attrs[name] = _read_attribute(name)
end
- attrs
end
# Filters the primary keys and readonly attributes from the attribute names.
@@ -441,9 +485,5 @@ module ActiveRecord
def pk_attribute?(name)
name == self.class.primary_key
end
-
- def typecasted_attribute_value(name)
- _read_attribute(name)
- end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 4f957ac3ca..7224f970e0 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "active_support/core_ext/module/attribute_accessors"
-require_relative "../attribute_mutation_tracker"
module ActiveRecord
module AttributeMethods
@@ -33,66 +32,13 @@ module ActiveRecord
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
@mutations_before_last_save = nil
- clear_mutation_trackers
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- end
- end
-
- def initialize_dup(other) # :nodoc:
- super
- @attributes = self.class._default_attributes.map do |attr|
- attr.with_value_from_user(@attributes.fetch_value(attr.name))
- end
- clear_mutation_trackers
- end
-
- def changes_applied # :nodoc:
- @mutations_before_last_save = mutation_tracker
- @mutations_from_database = AttributeMutationTracker.new(@attributes)
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- forget_attribute_assignments
- clear_mutation_trackers
- end
-
- def clear_changes_information # :nodoc:
- @mutations_before_last_save = nil
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- forget_attribute_assignments
- clear_mutation_trackers
- end
-
- def clear_attribute_changes(attr_names) # :nodoc:
- super
- attr_names.each do |attr_name|
- clear_attribute_change(attr_name)
- end
- end
-
- def changed_attributes # :nodoc:
- # This should only be set by methods which will call changed_attributes
- # multiple times when it is known that the computed value cannot change.
- if defined?(@cached_changed_attributes)
- @cached_changed_attributes
- else
- super.reverse_merge(mutation_tracker.changed_values).freeze
- end
- end
-
- def changes # :nodoc:
- cache_changed_attributes do
- super
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
+ @mutations_from_database = nil
end
end
- def previous_changes # :nodoc:
- mutations_before_last_save.changes
- end
-
- def attribute_changed_in_place?(attr_name) # :nodoc:
- mutation_tracker.changed_in_place?(attr_name)
- end
-
# Did this attribute change when we last saved? This method can be invoked
# as +saved_change_to_name?+ instead of <tt>saved_change_to_attribute?("name")</tt>.
# Behaves similarly to +attribute_changed?+. This method is useful in
@@ -168,7 +114,7 @@ module ActiveRecord
# Alias for +changed+
def changed_attribute_names_to_save
- changes_to_save.keys
+ mutations_from_database.changed_attribute_names
end
# Alias for +changed_attributes+
@@ -183,34 +129,6 @@ module ActiveRecord
result
end
- def mutation_tracker
- unless defined?(@mutation_tracker)
- @mutation_tracker = nil
- end
- @mutation_tracker ||= AttributeMutationTracker.new(@attributes)
- end
-
- def mutations_from_database
- unless defined?(@mutations_from_database)
- @mutations_from_database = nil
- end
- @mutations_from_database ||= mutation_tracker
- end
-
- def changes_include?(attr_name)
- super || mutation_tracker.changed?(attr_name)
- end
-
- def clear_attribute_change(attr_name)
- mutation_tracker.forget_change(attr_name)
- mutations_from_database.forget_change(attr_name)
- end
-
- def attribute_will_change!(attr_name)
- super
- mutations_from_database.force_change(attr_name)
- end
-
def _update_record(*)
partial_writes? ? super(keys_for_partial_write) : super
end
@@ -222,30 +140,6 @@ module ActiveRecord
def keys_for_partial_write
changed_attribute_names_to_save & self.class.column_names
end
-
- def forget_attribute_assignments
- @attributes = @attributes.map(&:forgetting_assignment)
- end
-
- def clear_mutation_trackers
- @mutation_tracker = nil
- @mutations_from_database = nil
- end
-
- def mutations_before_last_save
- @mutations_before_last_save ||= NullMutationTracker.instance
- end
-
- def cache_changed_attributes
- @cached_changed_attributes = changed_attributes
- yield
- ensure
- clear_changed_attributes_cache
- end
-
- def clear_changed_attributes_cache
- remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
- end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 63c059e291..9b267bb7c0 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -17,13 +17,15 @@ module ActiveRecord
# Returns the primary key value.
def id
sync_with_transaction_state
- _read_attribute(self.class.primary_key) if self.class.primary_key
+ primary_key = self.class.primary_key
+ _read_attribute(primary_key) if primary_key
end
# Sets the primary key value.
def id=(value)
sync_with_transaction_state
- _write_attribute(self.class.primary_key, value) if self.class.primary_key
+ primary_key = self.class.primary_key
+ _write_attribute(primary_key, value) if primary_key
end
# Queries the primary key value.
@@ -81,7 +83,7 @@ module ActiveRecord
end
def reset_primary_key #:nodoc:
- if self == base_class
+ if base_class?
self.primary_key = get_primary_key(base_class.name)
else
self.primary_key = base_class.primary_key
@@ -129,7 +131,7 @@ module ActiveRecord
def suppress_composite_primary_key(pk)
return pk unless pk.is_a?(Array)
- warn <<-WARNING.strip_heredoc
+ warn <<~WARNING
WARNING: Active Record does not support composite primary key.
#{table_name} has composite primary key. Composite primary key is ignored.
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index b070235684..0f7bcba564 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -27,7 +27,7 @@ module ActiveRecord
# Making it frozen means that it doesn't get duped when used to
# key the @attributes in read_attribute.
def define_method_attribute(name)
- safe_name = name.unpack("h*".freeze).first
+ safe_name = name.unpack1("h*".freeze)
temp_method = "__temp__#{safe_name}"
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
@@ -58,8 +58,9 @@ module ActiveRecord
attr_name.to_s
end
- name = self.class.primary_key if name == "id".freeze && self.class.primary_key
- sync_with_transaction_state if name == self.class.primary_key
+ primary_key = self.class.primary_key
+ name = primary_key if name == "id".freeze && primary_key
+ sync_with_transaction_state if name == primary_key
_read_attribute(name, &block)
end
@@ -68,7 +69,7 @@ module ActiveRecord
if defined?(JRUBY_VERSION)
# This form is significantly faster on JRuby, and this is one of our biggest hotspots.
# https://github.com/jruby/jruby/pull/2562
- def _read_attribute(attr_name, &block) # :nodoc
+ def _read_attribute(attr_name, &block) # :nodoc:
@attributes.fetch_value(attr_name.to_s, &block)
end
else
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index ebc2baed34..6e0e90f39c 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -7,7 +7,7 @@ module ActiveRecord
class ColumnNotSerializableError < StandardError
def initialize(name, type)
- super <<-EOS.strip_heredoc
+ super <<~EOS
Column `#{name}` of type #{type.class} does not support `serialize` feature.
Usually it means that you are trying to use `serialize`
on a column that already implements serialization natively.
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 37891ce2ef..c7521422bb 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -13,7 +13,7 @@ module ActiveRecord
private
def define_method_attribute=(name)
- safe_name = name.unpack("h*".freeze).first
+ safe_name = name.unpack1("h*".freeze)
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
@@ -39,8 +39,9 @@ module ActiveRecord
attr_name.to_s
end
- name = self.class.primary_key if name == "id".freeze && self.class.primary_key
- sync_with_transaction_state if name == self.class.primary_key
+ primary_key = self.class.primary_key
+ name = primary_key if name == "id".freeze && primary_key
+ sync_with_transaction_state if name == primary_key
_write_attribute(name, value)
end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index afb559db71..35150889d9 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "attribute/user_provided_default"
+require "active_model/attribute/user_provided_default"
module ActiveRecord
# See ActiveRecord::Attributes::ClassMethods for documentation
@@ -57,7 +57,7 @@ module ActiveRecord
# store_listing = StoreListing.new(price_in_cents: '10.1')
#
# # before
- # store_listing.price_in_cents # => BigDecimal.new(10.1)
+ # store_listing.price_in_cents # => BigDecimal(10.1)
#
# class StoreListing < ActiveRecord::Base
# attribute :price_in_cents, :integer
@@ -250,14 +250,14 @@ module ActiveRecord
if value == NO_DEFAULT_PROVIDED
default_attribute = _default_attributes[name].with_type(type)
elsif from_user
- default_attribute = Attribute::UserProvidedDefault.new(
+ default_attribute = ActiveModel::Attribute::UserProvidedDefault.new(
name,
value,
type,
_default_attributes.fetch(name.to_s) { nil },
)
else
- default_attribute = Attribute.from_database(name, value, type)
+ default_attribute = ActiveModel::Attribute.from_database(name, value, type)
end
_default_attributes[name] = default_attribute
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 6974cf74f6..a1250c3835 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -436,6 +436,9 @@ module ActiveRecord
if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key)
unless reflection.through_reflection
record[reflection.foreign_key] = key
+ if inverse_reflection = reflection.inverse_of
+ record.association(inverse_reflection.name).loaded!
+ end
end
saved = record.save(validate: !autosave)
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 541ff51fbe..7ab9160265 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -9,20 +9,19 @@ require "active_support/core_ext/module/attribute_accessors"
require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/hash/deep_merge"
require "active_support/core_ext/hash/slice"
-require "active_support/core_ext/hash/transform_values"
require "active_support/core_ext/string/behavior"
require "active_support/core_ext/kernel/singleton_class"
require "active_support/core_ext/module/introspection"
require "active_support/core_ext/object/duplicable"
require "active_support/core_ext/class/subclasses"
-require_relative "attribute_decorators"
-require_relative "define_callbacks"
-require_relative "errors"
-require_relative "log_subscriber"
-require_relative "explain_subscriber"
-require_relative "relation/delegation"
-require_relative "attributes"
-require_relative "type_caster"
+require "active_record/attribute_decorators"
+require "active_record/define_callbacks"
+require "active_record/errors"
+require "active_record/log_subscriber"
+require "active_record/explain_subscriber"
+require "active_record/relation/delegation"
+require "active_record/attributes"
+require "active_record/type_caster"
module ActiveRecord #:nodoc:
# = Active Record
@@ -291,6 +290,7 @@ module ActiveRecord #:nodoc:
extend CollectionCacheKey
include Core
+ include DatabaseConfigurations
include Persistence
include ReadonlyAttributes
include ModelSchema
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index a2439e6ec7..fd6819d08f 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -75,21 +75,7 @@ module ActiveRecord
# end
#
# Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is
- # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation
- # where the +before_destroy+ method is overridden:
- #
- # class Topic < ActiveRecord::Base
- # def before_destroy() destroy_author end
- # end
- #
- # class Reply < Topic
- # def before_destroy() destroy_readers end
- # end
- #
- # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
- # So, use the callback macros when you want to ensure that a certain callback is called for the entire
- # hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant
- # to decide whether they want to call +super+ and trigger the inherited callbacks.
+ # run, both +destroy_author+ and +destroy_readers+ are called.
#
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
# callbacks before specifying the associations. Otherwise, you might trigger the loading of a
@@ -98,9 +84,9 @@ module ActiveRecord
# == Types of callbacks
#
# There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
- # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects
+ # inline methods (using a proc). Method references and callback objects
# are the recommended approaches, inline methods using a proc are sometimes appropriate (such as for
- # creating mix-ins), and inline eval methods are deprecated.
+ # creating mix-ins).
#
# The method reference callbacks work by specifying a protected or private method available in the object, like this:
#
@@ -232,7 +218,7 @@ module ActiveRecord
#
# For example:
#
- # class Topic
+ # class Topic < ActiveRecord::Base
# has_many :children
#
# after_save :log_children
@@ -240,7 +226,7 @@ module ActiveRecord
#
# private
#
- # def log_chidren
+ # def log_children
# # Child processing
# end
#
@@ -257,7 +243,7 @@ module ActiveRecord
#
# For example:
#
- # class Topic
+ # class Topic < ActiveRecord::Base
# has_many :children
#
# after_commit :log_children
@@ -265,7 +251,7 @@ module ActiveRecord
#
# private
#
- # def log_chidren
+ # def log_children
# # Child processing
# end
#
diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb
index f3e6516414..dfba78614e 100644
--- a/activerecord/lib/active_record/collection_cache_key.rb
+++ b/activerecord/lib/active_record/collection_cache_key.rb
@@ -3,22 +3,24 @@
module ActiveRecord
module CollectionCacheKey
def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
- query_signature = Digest::MD5.hexdigest(collection.to_sql)
+ query_signature = ActiveSupport::Digest.hexdigest(collection.to_sql)
key = "#{collection.model_name.cache_key}/query-#{query_signature}"
- if collection.loaded?
- size = collection.size
+ if collection.loaded? || collection.distinct_value
+ size = collection.records.size
if size > 0
timestamp = collection.max_by(&timestamp_column)._read_attribute(timestamp_column)
end
else
- column_type = type_for_attribute(timestamp_column.to_s)
+ if collection.eager_loading?
+ collection = collection.send(:apply_join_dependency)
+ end
+ column_type = type_for_attribute(timestamp_column)
column = connection.column_name_from_arel_node(collection.arel_attribute(timestamp_column))
select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp"
if collection.has_limit_or_offset?
- query = collection.spawn
- query.select_values = [column]
+ query = collection.select(column)
subquery_alias = "subquery_for_cache_key"
subquery_column = "#{subquery_alias}.#{timestamp_column}"
subquery = query.arel.as(subquery_alias)
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 0759f4d2b3..c730584902 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -63,15 +63,13 @@ module ActiveRecord
# There are several connection-pooling-related options that you can add to
# your database connection configuration:
#
- # * +pool+: number indicating size of connection pool (default 5)
- # * +checkout_timeout+: number of seconds to block and wait for a connection
- # before giving up and raising a timeout error (default 5 seconds).
- # * +reaping_frequency+: frequency in seconds to periodically run the
- # Reaper, which attempts to find and recover connections from dead
- # threads, which can occur if a programmer forgets to close a
- # connection at the end of a thread or a thread dies unexpectedly.
- # Regardless of this setting, the Reaper will be invoked before every
- # blocking wait. (Default +nil+, which means don't schedule the Reaper).
+ # * +pool+: maximum number of connections the pool may manage (default 5).
+ # * +idle_timeout+: number of seconds that a connection will be kept
+ # unused in the pool before it is automatically disconnected (default
+ # 300 seconds). Set this to zero to keep connections forever.
+ # * +checkout_timeout+: number of seconds to wait for a connection to
+ # become available before giving up and raising a timeout error (default
+ # 5 seconds).
#
#--
# Synchronization policy:
@@ -82,11 +80,8 @@ module ActiveRecord
# * private methods that require being called in a +synchronize+ blocks
# are now explicitly documented
class ConnectionPool
- # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool
- # with which it shares a Monitor. But could be a generic Queue.
- #
- # The Queue in stdlib's 'thread' could replace this class except
- # stdlib's doesn't support waiting with a timeout.
+ # Threadsafe, fair, LIFO queue. Meant to be used by ConnectionPool
+ # with which it shares a Monitor.
class Queue
def initialize(lock = Monitor.new)
@lock = lock
@@ -175,7 +170,7 @@ module ActiveRecord
# Removes and returns the head of the queue if possible, or +nil+.
def remove
- @queue.shift
+ @queue.pop
end
# Remove and return the head the queue if the number of
@@ -283,12 +278,12 @@ module ActiveRecord
end
end
- # Every +frequency+ seconds, the reaper will call +reap+ on +pool+.
- # A reaper instantiated with a +nil+ frequency will never reap the
- # connection pool.
+ # Every +frequency+ seconds, the reaper will call +reap+ and +flush+ on
+ # +pool+. A reaper instantiated with a zero frequency will never reap
+ # the connection pool.
#
- # Configure the frequency by setting "reaping_frequency" in your
- # database yaml file.
+ # Configure the frequency by setting +reaping_frequency+ in your database
+ # yaml file (default 60 seconds).
class Reaper
attr_reader :pool, :frequency
@@ -298,11 +293,12 @@ module ActiveRecord
end
def run
- return unless frequency
+ return unless frequency && frequency > 0
Thread.new(frequency, pool) { |t, p|
loop do
sleep t
p.reap
+ p.flush
end
}
end
@@ -326,6 +322,10 @@ module ActiveRecord
@spec = spec
@checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5
+ if @idle_timeout = spec.config.fetch(:idle_timeout, 300)
+ @idle_timeout = @idle_timeout.to_f
+ @idle_timeout = nil if @idle_timeout <= 0
+ end
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
@@ -356,7 +356,10 @@ module ActiveRecord
@lock_thread = false
- @reaper = Reaper.new(self, spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f)
+ # +reaping_frequency+ is configurable mostly for historical reasons, but it could
+ # also be useful if someone wants a very low +idle_timeout+.
+ reaping_frequency = spec.config.fetch(:reaping_frequency, 60)
+ @reaper = Reaper.new(self, reaping_frequency && reaping_frequency.to_f)
@reaper.run
end
@@ -450,6 +453,21 @@ module ActiveRecord
disconnect(false)
end
+ # Discards all connections in the pool (even if they're currently
+ # leased!), along with the pool itself. Any further interaction with the
+ # pool (except #spec and #schema_cache) is undefined.
+ #
+ # See AbstractAdapter#discard!
+ def discard! # :nodoc:
+ synchronize do
+ return if @connections.nil? # already discarded
+ @connections.each do |conn|
+ conn.discard!
+ end
+ @connections = @available = @thread_cached_conns = nil
+ end
+ end
+
# Clears the cache which maps classes and re-connects connections that
# require reloading.
#
@@ -575,6 +593,35 @@ module ActiveRecord
end
end
+ # Disconnect all connections that have been idle for at least
+ # +minimum_idle+ seconds. Connections currently checked out, or that were
+ # checked in less than +minimum_idle+ seconds ago, are unaffected.
+ def flush(minimum_idle = @idle_timeout)
+ return if minimum_idle.nil?
+
+ idle_connections = synchronize do
+ @connections.select do |conn|
+ !conn.in_use? && conn.seconds_idle >= minimum_idle
+ end.each do |conn|
+ conn.lease
+
+ @available.delete conn
+ @connections.delete conn
+ end
+ end
+
+ idle_connections.each do |conn|
+ conn.disconnect!
+ end
+ end
+
+ # Disconnect all currently idle connections. Connections currently checked
+ # out are unaffected.
+ def flush!
+ reap
+ flush(-1)
+ end
+
def num_waiting_in_queue # :nodoc:
@available.num_waiting
end
@@ -866,11 +913,31 @@ module ActiveRecord
# about the model. The model needs to pass a specification name to the handler,
# in order to look up the correct connection pool.
class ConnectionHandler
+ def self.unowned_pool_finalizer(pid_map) # :nodoc:
+ lambda do |_|
+ discard_unowned_pools(pid_map)
+ end
+ end
+
+ def self.discard_unowned_pools(pid_map) # :nodoc:
+ pid_map.each do |pid, pools|
+ pools.values.compact.each(&:discard!) unless pid == Process.pid
+ end
+ end
+
def initialize
# These caches are keyed by spec.name (ConnectionSpecification#name).
@owner_to_pool = Concurrent::Map.new(initial_capacity: 2) do |h, k|
+ # Discard the parent's connection pools immediately; we have no need
+ # of them
+ ConnectionHandler.discard_unowned_pools(h)
+
h[k] = Concurrent::Map.new(initial_capacity: 2)
end
+
+ # Backup finalizer: if the forked child never needed a pool, the above
+ # early discard has not occurred
+ ObjectSpace.define_finalizer self, ConnectionHandler.unowned_pool_finalizer(@owner_to_pool)
end
def connection_pool_list
@@ -924,6 +991,13 @@ module ActiveRecord
connection_pool_list.each(&:disconnect!)
end
+ # Disconnects all currently idle connections.
+ #
+ # See ConnectionPool#flush! for details.
+ def flush_idle_connections!
+ connection_pool_list.each(&:flush!)
+ end
+
# Locate the connection of the nearest super class. This can be an
# active or defined connection: if it is the latter, it will be
# opened and set as the active connection for the class it was defined
@@ -931,9 +1005,7 @@ module ActiveRecord
def retrieve_connection(spec_name) #:nodoc:
pool = retrieve_connection_pool(spec_name)
raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool
- conn = pool.connection
- raise ConnectionNotEstablished, "No connection for '#{spec_name}' in connection pool" unless conn
- conn
+ pool.connection
end
# Returns true if a connection that's accessible to this class has
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 36048bee03..41553cfa83 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -356,38 +356,36 @@ module ActiveRecord
# Inserts a set of fixtures into the table. Overridden in adapters that require
# something beyond a simple insert (eg. Oracle).
def insert_fixtures(fixtures, table_name)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `insert_fixtures` is deprecated and will be removed in the next version of Rails.
+ Consider using `insert_fixtures_set` for performance improvement.
+ MSG
return if fixtures.empty?
- columns = schema_cache.columns_hash(table_name)
+ execute(build_fixture_sql(fixtures, table_name), "Fixtures Insert")
+ end
- values = fixtures.map do |fixture|
- fixture = fixture.stringify_keys
+ def insert_fixtures_set(fixture_set, tables_to_delete = [])
+ fixture_inserts = fixture_set.map do |table_name, fixtures|
+ next if fixtures.empty?
- unknown_columns = fixture.keys - columns.keys
- if unknown_columns.any?
- raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.)
- end
+ build_fixture_sql(fixtures, table_name)
+ end.compact
- columns.map do |name, column|
- if fixture.key?(name)
- type = lookup_cast_type_from_column(column)
- bind = Relation::QueryAttribute.new(name, fixture[name], type)
- with_yaml_fallback(bind.value_for_database)
- else
- Arel.sql("DEFAULT")
+ table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}".dup }
+ total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts))
+
+ disable_referential_integrity do
+ transaction(requires_new: true) do
+ total_sql.each do |sql|
+ execute sql, "Fixtures Load"
+ yield if block_given?
end
end
end
-
- table = Arel::Table.new(table_name)
- manager = Arel::InsertManager.new
- manager.into(table)
- columns.each_key { |column| manager.columns << table[column] }
- manager.values = manager.create_values_list(values)
- execute manager.to_sql, "Fixtures Insert"
end
- def empty_insert_statement_value
+ def empty_insert_statement_value(primary_key = nil)
"DEFAULT VALUES"
end
@@ -416,6 +414,44 @@ module ActiveRecord
alias join_to_delete join_to_update
private
+ def default_insert_value(column)
+ Arel.sql("DEFAULT")
+ end
+
+ def build_fixture_sql(fixtures, table_name)
+ columns = schema_cache.columns_hash(table_name)
+
+ values = fixtures.map do |fixture|
+ fixture = fixture.stringify_keys
+
+ unknown_columns = fixture.keys - columns.keys
+ if unknown_columns.any?
+ raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.)
+ end
+
+ columns.map do |name, column|
+ if fixture.key?(name)
+ type = lookup_cast_type_from_column(column)
+ bind = Relation::QueryAttribute.new(name, fixture[name], type)
+ with_yaml_fallback(bind.value_for_database)
+ else
+ default_insert_value(column)
+ end
+ end
+ end
+
+ table = Arel::Table.new(table_name)
+ manager = Arel::InsertManager.new
+ manager.into(table)
+ columns.each_key { |column| manager.columns << table[column] }
+ manager.values = manager.create_values_list(values)
+
+ manager.to_sql
+ end
+
+ def combine_multi_statements(total_sql)
+ total_sql.join(";\n")
+ end
# Returns a subquery for the given key using the join information.
def subquery_for(key, select)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 9ad04c3216..aec5fa6ba1 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -11,19 +11,6 @@ module ActiveRecord
def quote(value)
value = id_value_for_database(value) if value.is_a?(Base)
- if value.respond_to?(:quoted_id)
- at = value.method(:quoted_id).source_location
- at &&= " at %s:%d" % at
-
- owner = value.method(:quoted_id).owner.to_s
- klass = value.class.to_s
- klass += "(#{owner})" unless owner == klass
-
- ActiveSupport::Deprecation.warn \
- "Defining #quoted_id is deprecated and will be ignored in Rails 5.2. (defined on #{klass}#{at})"
- return value.quoted_id
- end
-
if value.respond_to?(:value_for_database)
value = value.value_for_database
end
@@ -37,10 +24,6 @@ module ActiveRecord
def type_cast(value, column = nil)
value = id_value_for_database(value) if value.is_a?(Base)
- if value.respond_to?(:quoted_id) && value.respond_to?(:id)
- return value.id
- end
-
if column
value = type_cast_from_column(column, value)
end
@@ -147,7 +130,7 @@ module ActiveRecord
end
def quoted_time(value) # :nodoc:
- quoted_date(value).sub(/\A2000-01-01 /, "")
+ quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "")
end
def quoted_binary(value) # :nodoc:
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 bd05fb8f6e..529c9d8ca6 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/string/strip"
-
module ActiveRecord
module ConnectionAdapters
class AbstractAdapter
@@ -17,9 +15,8 @@ module ActiveRecord
end
delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
- :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options, to: :@conn
- private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
- :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options
+ :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options,
+ to: :@conn, private: true
private
@@ -66,7 +63,7 @@ module ActiveRecord
end
def visit_ForeignKeyDefinition(o)
- sql = <<-SQL.strip_heredoc
+ sql = +<<~SQL
CONSTRAINT #{quote_column_name(o.name)}
FOREIGN KEY (#{quote_column_name(o.column)})
REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)})
@@ -95,6 +92,7 @@ module ActiveRecord
if options_sql = options[:options]
create_sql << " #{options_sql}"
end
+ create_sql
end
def column_options(o)
@@ -132,7 +130,7 @@ module ActiveRecord
when :cascade then "ON #{action} CASCADE"
when :restrict then "ON #{action} RESTRICT"
else
- raise ArgumentError, <<-MSG.strip_heredoc
+ raise ArgumentError, <<~MSG
'#{dependency}' is not supported for :on_update or :on_delete.
Supported values are: :nullify, :cascade, :restrict
MSG
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 788a455773..6a498b353c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -6,7 +6,7 @@ module ActiveRecord
# this type are typically created and returned by methods in database
# adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes
class IndexDefinition # :nodoc:
- attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment
+ attr_reader :table, :name, :unique, :columns, :lengths, :orders, :opclasses, :where, :type, :using, :comment
def initialize(
table, name,
@@ -14,6 +14,7 @@ module ActiveRecord
columns = [],
lengths: {},
orders: {},
+ opclasses: {},
where: nil,
type: nil,
using: nil,
@@ -23,13 +24,23 @@ module ActiveRecord
@name = name
@unique = unique
@columns = columns
- @lengths = lengths
- @orders = orders
+ @lengths = concise_options(lengths)
+ @orders = concise_options(orders)
+ @opclasses = concise_options(opclasses)
@where = where
@type = type
@using = using
@comment = comment
end
+
+ private
+ def concise_options(options)
+ if columns.size == options.size && options.values.uniq.size == 1
+ options.values.first
+ else
+ options
+ end
+ end
end
# Abstract representation of a column definition. Instances of this type
@@ -85,6 +96,15 @@ module ActiveRecord
options[:primary_key] != default_primary_key
end
+ def validate?
+ options.fetch(:validate, true)
+ end
+ alias validated? validate?
+
+ def export_name_on_schema_dump?
+ name !~ ActiveRecord::SchemaDumper.fk_ignore_pattern
+ end
+
def defined_for?(to_table_ord = nil, to_table: nil, **options)
if to_table_ord
self.to_table == to_table_ord.to_s
@@ -135,13 +155,8 @@ module ActiveRecord
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options
-
private
+ attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options
def as_options(value)
value.is_a?(Hash) ? value : {}
@@ -204,6 +219,7 @@ module ActiveRecord
:decimal,
:float,
:integer,
+ :json,
:string,
:text,
:time,
@@ -396,6 +412,9 @@ module ActiveRecord
alias :belongs_to :references
def new_column_definition(name, type, **options) # :nodoc:
+ if integer_like_primary_key?(type, options)
+ type = integer_like_primary_key_type(type, options)
+ end
type = aliased_types(type.to_s, type)
options[:primary_key] ||= type == :primary_key
options[:null] = false if options[:primary_key]
@@ -410,6 +429,14 @@ module ActiveRecord
def aliased_types(name, fallback)
"timestamp" == name ? :datetime : fallback
end
+
+ def integer_like_primary_key?(type, options)
+ options[:primary_key] && [:integer, :bigint].include?(type) && !options.key?(:default)
+ end
+
+ def integer_like_primary_key_type(type, options)
+ type
+ end
end
class AlterTable # :nodoc:
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 1926603474..622e00fffb 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/hash/compact"
-
module ActiveRecord
module ConnectionAdapters # :nodoc:
class SchemaDumper < SchemaDumper # :nodoc:
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 f57c7a5d4d..ac73337aef 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "../../migration/join_table"
+require "active_record/migration/join_table"
require "active_support/core_ext/string/access"
-require "digest"
+require "digest/sha2"
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -79,7 +79,7 @@ module ActiveRecord
end
# Returns an array of indexes for the given table.
- def indexes(table_name, name = nil)
+ def indexes(table_name)
raise NotImplementedError, "#indexes is not implemented"
end
@@ -216,7 +216,7 @@ module ActiveRecord
# generates:
#
# CREATE TABLE suppliers (
- # id int auto_increment PRIMARY KEY
+ # id bigint auto_increment PRIMARY KEY
# ) ENGINE=InnoDB DEFAULT CHARSET=utf8
#
# ====== Rename the primary key column
@@ -228,7 +228,7 @@ module ActiveRecord
# generates:
#
# CREATE TABLE objects (
- # guid int auto_increment PRIMARY KEY,
+ # guid bigint auto_increment PRIMARY KEY,
# name varchar(80)
# )
#
@@ -255,8 +255,8 @@ module ActiveRecord
# generates:
#
# CREATE TABLE order (
- # product_id integer NOT NULL,
- # client_id integer NOT NULL
+ # product_id bigint NOT NULL,
+ # client_id bigint NOT NULL
# );
#
# ALTER TABLE ONLY "orders"
@@ -265,15 +265,15 @@ module ActiveRecord
# ====== Do not add a primary key column
#
# create_table(:categories_suppliers, id: false) do |t|
- # t.column :category_id, :integer
- # t.column :supplier_id, :integer
+ # t.column :category_id, :bigint
+ # t.column :supplier_id, :bigint
# end
#
# generates:
#
# CREATE TABLE categories_suppliers (
- # category_id int,
- # supplier_id int
+ # category_id bigint,
+ # supplier_id bigint
# )
#
# ====== Create a temporary table based on a query
@@ -305,7 +305,8 @@ module ActiveRecord
yield td if block_given?
if options[:force]
- drop_table(table_name, **options, if_exists: true)
+ drop_opts = { if_exists: true }.merge(**options)
+ drop_table(table_name, drop_opts)
end
result = execute schema_creation.accept td
@@ -361,8 +362,8 @@ module ActiveRecord
# generates:
#
# CREATE TABLE assemblies_parts (
- # assembly_id int NOT NULL,
- # part_id int NOT NULL,
+ # assembly_id bigint NOT NULL,
+ # part_id bigint NOT NULL,
# ) ENGINE=InnoDB DEFAULT CHARSET=utf8
#
def create_join_table(table_1, table_2, column_options: {}, **options)
@@ -406,7 +407,7 @@ module ActiveRecord
#
# Defaults to false.
#
- # Only supported on the MySQL adapter, ignored elsewhere.
+ # Only supported on the MySQL and PostgreSQL adapter, ignored elsewhere.
#
# ====== Add a column
#
@@ -432,7 +433,7 @@ module ActiveRecord
# t.references :company
# end
#
- # Creates a <tt>company_id(integer)</tt> column.
+ # Creates a <tt>company_id(bigint)</tt> column.
#
# ====== Add a polymorphic foreign key column
#
@@ -440,7 +441,7 @@ module ActiveRecord
# t.belongs_to :company, polymorphic: true
# end
#
- # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns.
+ # Creates <tt>company_type(varchar)</tt> and <tt>company_id(bigint)</tt> columns.
#
# ====== Remove a column
#
@@ -522,6 +523,8 @@ module ActiveRecord
# Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns.
# * <tt>:scale</tt> -
# Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns.
+ # * <tt>:comment</tt> -
+ # Specifies the comment for the column. This option is ignored by some backends.
#
# Note: The precision is the total number of significant digits,
# and the scale is the number of digits that can be stored following
@@ -598,7 +601,7 @@ module ActiveRecord
# to provide these in a migration's +change+ method so it can be reverted.
# In that case, +type+ and +options+ will be used by #add_column.
def remove_column(table_name, column_name, type = nil, options = {})
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{remove_column_for_alter(table_name, column_name, type, options)}"
end
# Changes the column's definition according to the new options.
@@ -713,7 +716,7 @@ module ActiveRecord
#
# CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
#
- # Note: MySQL doesn't yet support index order (it accepts the syntax but ignores it).
+ # Note: MySQL only supports index order from 8.0.1 onwards (earlier versions accepted the syntax but ignored it).
#
# ====== Creating a partial index
#
@@ -736,6 +739,28 @@ module ActiveRecord
#
# Note: only supported by PostgreSQL and MySQL
#
+ # ====== Creating an index with a specific operator class
+ #
+ # add_index(:developers, :name, using: 'gist', opclass: :gist_trgm_ops)
+ #
+ # generates:
+ #
+ # CREATE INDEX developers_on_name ON developers USING gist (name gist_trgm_ops) -- PostgreSQL
+ #
+ # add_index(:developers, [:name, :city], using: 'gist', opclass: { city: :gist_trgm_ops })
+ #
+ # generates:
+ #
+ # CREATE INDEX developers_on_name_and_city ON developers USING gist (name, city gist_trgm_ops) -- PostgreSQL
+ #
+ # add_index(:developers, [:name, :city], using: 'gist', opclass: :gist_trgm_ops)
+ #
+ # generates:
+ #
+ # CREATE INDEX developers_on_name_and_city ON developers USING gist (name gist_trgm_ops, city gist_trgm_ops) -- PostgreSQL
+ #
+ # Note: only supported by PostgreSQL
+ #
# ====== Creating an index with a specific type
#
# add_index(:developers, :name, type: :fulltext)
@@ -804,24 +829,19 @@ module ActiveRecord
end
# Verifies the existence of an index with a given name.
- def index_name_exists?(table_name, index_name, default = nil)
- unless default.nil?
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing default to #index_name_exists? is deprecated without replacement.
- MSG
- end
+ def index_name_exists?(table_name, index_name)
index_name = index_name.to_s
indexes(table_name).detect { |i| i.name == index_name }
end
- # Adds a reference. The reference column is an integer by default,
+ # Adds a reference. The reference column is a bigint by default,
# the <tt>:type</tt> option can be used to specify a different type.
# Optionally adds a +_type+ column, if <tt>:polymorphic</tt> option is provided.
# #add_reference and #add_belongs_to are acceptable.
#
# The +options+ hash can include the following keys:
# [<tt>:type</tt>]
- # The reference column type. Defaults to +:integer+.
+ # The reference column type. Defaults to +:bigint+.
# [<tt>:index</tt>]
# Add an appropriate index. Defaults to true.
# See #add_index for usage of this option.
@@ -832,7 +852,7 @@ module ActiveRecord
# [<tt>:null</tt>]
# Whether the column allows nulls. Defaults to true.
#
- # ====== Create a user_id integer column
+ # ====== Create a user_id bigint column
#
# add_reference(:products, :user)
#
@@ -889,7 +909,7 @@ module ActiveRecord
foreign_key_options = { to_table: reference_name }
end
foreign_key_options[:column] ||= "#{ref_name}_id"
- remove_foreign_key(table_name, **foreign_key_options)
+ remove_foreign_key(table_name, foreign_key_options)
end
remove_column(table_name, "#{ref_name}_id")
@@ -945,6 +965,8 @@ module ActiveRecord
# Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
# [<tt>:on_update</tt>]
# Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
+ # [<tt>:validate</tt>]
+ # (Postgres only) Specify whether or not the constraint should be validated. Defaults to +true+.
def add_foreign_key(from_table, to_table, options = {})
return unless supports_foreign_keys?
@@ -1018,16 +1040,6 @@ module ActiveRecord
insert_versions_sql(versions) if versions.any?
end
- def initialize_schema_migrations_table # :nodoc:
- ActiveRecord::SchemaMigration.create_table
- end
- deprecate :initialize_schema_migrations_table
-
- def initialize_internal_metadata_table # :nodoc:
- ActiveRecord::InternalMetadata.create_table
- end
- deprecate :initialize_internal_metadata_table
-
def internal_string_options_for_primary_key # :nodoc:
{ primary_key: true }
end
@@ -1038,8 +1050,8 @@ module ActiveRecord
sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name)
migrated = ActiveRecord::SchemaMigration.all_versions.map(&:to_i)
- versions = ActiveRecord::Migrator.migration_files(migrations_paths).map do |file|
- ActiveRecord::Migrator.parse_migration_filename(file).first.to_i
+ versions = migration_context.migration_files.map do |file|
+ migration_context.parse_migration_filename(file).first.to_i
end
unless migrated.include?(version)
@@ -1133,7 +1145,7 @@ module ActiveRecord
def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc:
column_names = index_column_names(column_name)
- options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
+ options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclass)
index_type = options[:type].to_s if options.key?(:type)
index_type ||= options[:unique] ? "UNIQUE" : ""
@@ -1172,7 +1184,7 @@ module ActiveRecord
end
# Changes the comment for a column or removes it if +nil+.
- def change_column_comment(table_name, column_name, comment) #:nodoc:
+ def change_column_comment(table_name, column_name, comment)
raise NotImplementedError, "#{self.class} does not support changing column comments"
end
@@ -1186,20 +1198,22 @@ module ActiveRecord
end
def add_index_sort_order(quoted_columns, **options)
- if order = options[:order]
- case order
- when Hash
- order = order.symbolize_keys
- quoted_columns.each { |name, column| column << " #{order[name].upcase}" if order[name].present? }
- when String
- quoted_columns.each { |name, column| column << " #{order.upcase}" if order.present? }
- end
+ orders = options_for_index_columns(options[:order])
+ quoted_columns.each do |name, column|
+ column << " #{orders[name].upcase}" if orders[name].present?
end
+ end
- quoted_columns
+ def options_for_index_columns(options)
+ if options.is_a?(Hash)
+ options.symbolize_keys
+ else
+ Hash.new { |hash, column| hash[column] = options }
+ end
end
- # Overridden by the MySQL adapter for supporting index lengths
+ # Overridden by the MySQL adapter for supporting index lengths and by
+ # the PostgreSQL adapter for supporting operator classes.
def add_options_for_index_columns(quoted_columns, **options)
if supports_index_sort_order?
quoted_columns = add_index_sort_order(quoted_columns, options)
@@ -1353,6 +1367,20 @@ module ActiveRecord
options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty?
end
+ def add_column_for_alter(table_name, column_name, type, options = {})
+ td = create_table_definition(table_name)
+ cd = td.new_column_definition(column_name, type, options)
+ schema_creation.accept(AddColumnDefinition.new(cd))
+ end
+
+ def remove_column_for_alter(table_name, column_name, type = nil, options = {})
+ "DROP COLUMN #{quote_column_name(column_name)}"
+ end
+
+ def remove_columns_for_alter(table_name, *column_names)
+ column_names.map { |column_name| remove_column_for_alter(table_name, column_name) }
+ end
+
def insert_versions_sql(versions)
sm_table = quote_table_name(ActiveRecord::SchemaMigration.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 147e16e9fa..0ce3796829 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -75,7 +75,6 @@ module ActiveRecord
class Transaction #:nodoc:
attr_reader :connection, :state, :records, :savepoint_name
- attr_writer :joinable
def initialize(connection, options, run_commit_callbacks: false)
@connection = connection
@@ -240,7 +239,7 @@ module ActiveRecord
rollback_transaction if transaction
else
begin
- commit_transaction
+ commit_transaction if transaction
rescue Exception
rollback_transaction(transaction) unless transaction.state.completed?
raise
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 8c889f98f5..559f068c39 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
-require_relative "../type"
-require_relative "determine_if_preparable_visitor"
-require_relative "schema_cache"
-require_relative "sql_type_metadata"
-require_relative "abstract/schema_dumper"
-require_relative "abstract/schema_creation"
+require "active_record/connection_adapters/determine_if_preparable_visitor"
+require "active_record/connection_adapters/schema_cache"
+require "active_record/connection_adapters/sql_type_metadata"
+require "active_record/connection_adapters/abstract/schema_dumper"
+require "active_record/connection_adapters/abstract/schema_creation"
+require "active_support/concurrency/load_interlock_aware_monitor"
require "arel/collectors/bind"
require "arel/collectors/composite"
require "arel/collectors/sql_string"
@@ -105,10 +105,11 @@ module ActiveRecord
@logger = logger
@config = config
@pool = nil
+ @idle_since = Concurrent.monotonic_time
@schema_cache = SchemaCache.new self
@quoted_column_names, @quoted_table_names = {}, {}
@visitor = arel_visitor
- @lock = Monitor.new
+ @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
@prepared_statements = true
@@ -118,6 +119,14 @@ module ActiveRecord
end
end
+ def migrations_paths # :nodoc:
+ @config[:migrations_paths] || Migrator.migrations_paths
+ end
+
+ def migration_context # :nodoc:
+ MigrationContext.new(migrations_paths)
+ end
+
class Version
include Comparable
@@ -164,6 +173,7 @@ module ActiveRecord
"Current thread: #{Thread.current}."
end
+ @idle_since = Concurrent.monotonic_time
@owner = nil
else
raise ActiveRecordError, "Cannot expire connection, it is not currently leased."
@@ -183,6 +193,12 @@ module ActiveRecord
end
end
+ # Seconds since this connection was returned to the pool
+ def seconds_idle # :nodoc:
+ return 0 if in_use?
+ Concurrent.monotonic_time - @idle_since
+ end
+
def unprepared_statement
old_prepared_statements, @prepared_statements = @prepared_statements, false
yield
@@ -196,16 +212,6 @@ module ActiveRecord
self.class::ADAPTER_NAME
end
- def supports_migrations? # :nodoc:
- true
- end
- deprecate :supports_migrations?
-
- def supports_primary_key? # :nodoc:
- true
- end
- deprecate :supports_primary_key?
-
# Does this adapter support DDL rollbacks in transactions? That is, would
# CREATE TABLE or ALTER TABLE get rolled back by a transaction?
def supports_ddl_transactions?
@@ -274,6 +280,11 @@ module ActiveRecord
false
end
+ # Does this adapter support creating invalid constraints?
+ def supports_validate_constraints?
+ false
+ end
+
# Does this adapter support creating foreign key constraints
# in the same statement as creating the table?
def supports_foreign_keys_in_create?
@@ -315,6 +326,11 @@ module ActiveRecord
false
end
+ # Does this adapter support foreign/external tables?
+ def supports_foreign_tables?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -377,6 +393,19 @@ module ActiveRecord
reset_transaction
end
+ # Immediately forget this connection ever existed. Unlike disconnect!,
+ # this will not communicate with the server.
+ #
+ # After calling this method, the behavior of all other methods becomes
+ # undefined. This is called internally just before a forked process gets
+ # rid of a connection that belonged to its parent.
+ def discard!
+ # This should be overridden by concrete adapters.
+ #
+ # Prevent @connection's finalizer from touching the socket, or
+ # otherwise communicating with its server, when it is collected.
+ end
+
# Reset the state of this connection, directing the DBMS to clear
# transactions and other connection-related server-side state. Usually a
# database-dependent operation.
@@ -402,10 +431,7 @@ module ActiveRecord
# Checks whether the connection to the database is still active (i.e. not stale).
# This is done under the hood by calling #active?. If the connection
# is no longer active, then this method will reconnect to the database.
- def verify!(*ignored)
- if ignored.size > 0
- ActiveSupport::Deprecation.warn("Passing arguments to #verify method of the connection has no effect and has been deprecated. Please remove all arguments from the #verify method call.")
- end
+ def verify!
reconnect! unless active?
end
@@ -478,6 +504,8 @@ module ActiveRecord
m.alias_type %r(number)i, "decimal"
m.alias_type %r(double)i, "float"
+ m.register_type %r(^json)i, Type::Json.new
+
m.register_type(%r(decimal)i) do |sql_type|
scale = extract_scale(sql_type)
precision = extract_precision(sql_type)
@@ -522,12 +550,7 @@ module ActiveRecord
end
def extract_limit(sql_type)
- case sql_type
- when /^bigint/i
- 8
- when /\((.*)\)/
- $1.to_i
- end
+ $1.to_i if sql_type =~ /\((.*)\)/
end
def translate_exception_class(e, sql)
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 7cd086084a..07acb5425e 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,17 +1,15 @@
# frozen_string_literal: true
-require_relative "abstract_adapter"
-require_relative "statement_pool"
-require_relative "mysql/column"
-require_relative "mysql/explain_pretty_printer"
-require_relative "mysql/quoting"
-require_relative "mysql/schema_creation"
-require_relative "mysql/schema_definitions"
-require_relative "mysql/schema_dumper"
-require_relative "mysql/schema_statements"
-require_relative "mysql/type_metadata"
-
-require "active_support/core_ext/string/strip"
+require "active_record/connection_adapters/abstract_adapter"
+require "active_record/connection_adapters/statement_pool"
+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"
+require "active_record/connection_adapters/mysql/schema_statements"
+require "active_record/connection_adapters/mysql/type_metadata"
module ActiveRecord
module ConnectionAdapters
@@ -44,9 +42,9 @@ module ActiveRecord
json: { name: "json" },
}
- class StatementPool < ConnectionAdapters::StatementPool
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
private def dealloc(stmt)
- stmt[:stmt].close
+ stmt.close
end
end
@@ -117,11 +115,11 @@ module ActiveRecord
end
def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
- query_value("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})") == 1
+ query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
end
def release_advisory_lock(lock_name) # :nodoc:
- query_value("SELECT RELEASE_LOCK(#{quote(lock_name)})") == 1
+ query_value("SELECT RELEASE_LOCK(#{quote(lock_name.to_s)})") == 1
end
def native_database_types
@@ -225,7 +223,7 @@ module ActiveRecord
end
end
- def empty_insert_statement_value
+ def empty_insert_statement_value(primary_key = nil)
"VALUES ()"
end
@@ -249,7 +247,7 @@ module ActiveRecord
# create_database 'matt_development', charset: :big5
def create_database(name, options = {})
if options[:collation]
- execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}"
+ execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}"
else
execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}"
end
@@ -284,7 +282,7 @@ module ActiveRecord
def table_comment(table_name) # :nodoc:
scope = quoted_scope(table_name)
- query_value(<<-SQL.strip_heredoc, "SCHEMA")
+ query_value(<<~SQL, "SCHEMA").presence
SELECT table_comment
FROM information_schema.tables
WHERE table_schema = #{scope[:schema]}
@@ -292,14 +290,10 @@ module ActiveRecord
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:
sqls = operations.flat_map do |command, args|
table, arguments = args.shift, args
- method = :"#{command}_sql"
+ method = :"#{command}_for_alter"
if respond_to?(method, true)
send(method, table, *arguments)
@@ -311,6 +305,11 @@ module ActiveRecord
execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
end
+ def change_table_comment(table_name, comment) #:nodoc:
+ comment = "" if comment.nil?
+ execute("ALTER TABLE #{quote_table_name(table_name)} COMMENT #{quote(comment)}")
+ end
+
# Renames a table.
#
# Example:
@@ -351,26 +350,27 @@ module ActiveRecord
def change_column_default(table_name, column_name, default_or_changes) #:nodoc:
default = extract_new_default_value(default_or_changes)
- column = column_for(table_name, column_name)
- change_column table_name, column_name, column.sql_type, default: default
+ change_column table_name, column_name, nil, default: default
end
def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
- column = column_for(table_name, column_name)
-
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
- change_column table_name, column_name, column.sql_type, null: null
+ change_column table_name, column_name, nil, null: null
+ end
+
+ def change_column_comment(table_name, column_name, comment) #:nodoc:
+ change_column table_name, column_name, nil, comment: comment
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
- execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_for_alter(table_name, column_name, type, options)}")
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
- execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_for_alter(table_name, column_name, new_column_name)}")
rename_column_indexes(table_name, column_name, new_column_name)
end
@@ -390,19 +390,20 @@ module ActiveRecord
scope = quoted_scope(table_name)
- fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA")
+ fk_info = exec_query(<<~SQL, "SCHEMA")
SELECT fk.referenced_table_name AS 'to_table',
fk.referenced_column_name AS 'primary_key',
fk.column_name AS 'column',
fk.constraint_name AS 'name',
rc.update_rule AS 'on_update',
rc.delete_rule AS 'on_delete'
- FROM information_schema.key_column_usage fk
- JOIN information_schema.referential_constraints rc
+ FROM information_schema.referential_constraints rc
+ JOIN information_schema.key_column_usage fk
USING (constraint_schema, constraint_name)
WHERE fk.referenced_column_name IS NOT NULL
AND fk.table_schema = #{scope[:schema]}
AND fk.table_name = #{scope[:name]}
+ AND rc.constraint_schema = #{scope[:schema]}
AND rc.table_name = #{scope[:name]}
SQL
@@ -477,7 +478,7 @@ module ActiveRecord
scope = quoted_scope(table_name)
- query_values(<<-SQL.strip_heredoc, "SCHEMA")
+ query_values(<<~SQL, "SCHEMA")
SELECT column_name
FROM information_schema.key_column_usage
WHERE constraint_name = 'PRIMARY'
@@ -512,7 +513,7 @@ module ActiveRecord
s.gsub(/\s+(?:ASC|DESC)\b/i, "")
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
- [super, *order_columns].join(", ")
+ (order_columns << super).join(", ")
end
def strict_mode?
@@ -523,23 +524,49 @@ module ActiveRecord
index.using == :btree || super
end
- def insert_fixtures(*)
- without_sql_mode("NO_AUTO_VALUE_ON_ZERO") { super }
+ def insert_fixtures_set(fixture_set, tables_to_delete = [])
+ with_multi_statements do
+ super { discard_remaining_results }
+ end
end
private
+ def combine_multi_statements(total_sql)
+ total_sql.each_with_object([]) do |sql, total_sql_chunks|
+ previous_packet = total_sql_chunks.last
+ sql << ";\n"
+ if max_allowed_packet_reached?(sql, previous_packet) || total_sql_chunks.empty?
+ total_sql_chunks << sql
+ else
+ previous_packet << sql
+ end
+ end
+ end
+
+ def max_allowed_packet_reached?(current_packet, previous_packet)
+ if current_packet.bytesize > max_allowed_packet
+ raise ActiveRecordError, "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable."
+ elsif previous_packet.nil?
+ false
+ else
+ (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet
+ end
+ end
- def without_sql_mode(mode)
- result = execute("SELECT @@SESSION.sql_mode")
- current_mode = result.first[0]
- return yield unless current_mode.include?(mode)
+ def max_allowed_packet
+ bytes_margin = 2
+ @max_allowed_packet ||= (show_variable("max_allowed_packet") - bytes_margin)
+ end
+
+ def with_multi_statements
+ previous_flags = @config[:flags]
+ @config[:flags] = Mysql2::Client::MULTI_STATEMENTS
+ reconnect!
- sql_mode = "REPLACE(@@sql_mode, '#{mode}', '')"
- execute("SET @@SESSION.sql_mode = #{sql_mode}")
yield
ensure
- sql_mode = "CONCAT(@@sql_mode, ',#{mode}')"
- execute("SET @@SESSION.sql_mode = #{sql_mode}")
+ @config[:flags] = previous_flags
+ reconnect!
end
def initialize_type_map(m = type_map)
@@ -557,7 +584,6 @@ module ActiveRecord
m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1)
m.register_type %r(^float)i, Type::Float.new(limit: 24)
m.register_type %r(^double)i, Type::Float.new(limit: 53)
- m.register_type %r(^json)i, Type::Json.new
register_integer_type m, %r(^bigint)i, limit: 8
register_integer_type m, %r(^int)i, limit: 4
@@ -600,25 +626,6 @@ module ActiveRecord
end
end
- def add_index_length(quoted_columns, **options)
- if length = options[:length]
- case length
- when Hash
- length = length.symbolize_keys
- quoted_columns.each { |name, column| column << "(#{length[name]})" if length[name].present? }
- when Integer
- quoted_columns.each { |name, column| column << "(#{length})" }
- end
- end
-
- quoted_columns
- end
-
- def add_options_for_index_columns(quoted_columns, **options)
- quoted_columns = add_index_length(quoted_columns, options)
- super
- end
-
# See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html
ER_DUP_ENTRY = 1062
ER_NOT_NULL_VIOLATION = 1048
@@ -630,6 +637,8 @@ module ActiveRecord
ER_CANNOT_ADD_FOREIGN = 1215
ER_CANNOT_CREATE_TABLE = 1005
ER_LOCK_WAIT_TIMEOUT = 1205
+ ER_QUERY_INTERRUPTED = 1317
+ ER_QUERY_TIMEOUT = 3024
def translate_exception(exception, message)
case error_number(exception)
@@ -654,20 +663,19 @@ module ActiveRecord
when ER_LOCK_DEADLOCK
Deadlocked.new(message)
when ER_LOCK_WAIT_TIMEOUT
- TransactionTimeout.new(message)
+ LockWaitTimeout.new(message)
+ when ER_QUERY_TIMEOUT
+ StatementTimeout.new(message)
+ when ER_QUERY_INTERRUPTED
+ QueryCanceled.new(message)
else
super
end
end
- def add_column_sql(table_name, column_name, type, options = {})
- td = create_table_definition(table_name)
- cd = td.new_column_definition(column_name, type, options)
- schema_creation.accept(AddColumnDefinition.new(cd))
- end
-
- def change_column_sql(table_name, column_name, type, options = {})
+ def change_column_for_alter(table_name, column_name, type, options = {})
column = column_for(table_name, column_name)
+ type ||= column.sql_type
unless options.key?(:default)
options[:default] = column.default
@@ -686,7 +694,7 @@ module ActiveRecord
schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
end
- def rename_column_sql(table_name, column_name, new_column_name)
+ def rename_column_for_alter(table_name, column_name, new_column_name)
column = column_for(table_name, column_name)
options = {
default: column.default,
@@ -700,31 +708,23 @@ module ActiveRecord
schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
end
- def remove_column_sql(table_name, column_name, type = nil, options = {})
- "DROP #{quote_column_name(column_name)}"
- end
-
- def remove_columns_sql(table_name, *column_names)
- column_names.map { |column_name| remove_column_sql(table_name, column_name) }
- end
-
- def add_index_sql(table_name, column_name, options = {})
+ def add_index_for_alter(table_name, column_name, options = {})
index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options)
index_algorithm[0, 0] = ", " if index_algorithm.present?
"ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}"
end
- def remove_index_sql(table_name, options = {})
+ def remove_index_for_alter(table_name, options = {})
index_name = index_name_for_remove(table_name, options)
- "DROP INDEX #{index_name}"
+ "DROP INDEX #{quote_column_name(index_name)}"
end
- def add_timestamps_sql(table_name, options = {})
- [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)]
+ def add_timestamps_for_alter(table_name, options = {})
+ [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
end
- def remove_timestamps_sql(table_name, options = {})
- [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
+ def remove_timestamps_for_alter(table_name, options = {})
+ [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)]
end
# MySQL is too stupid to create a temporary table for use subquery, so we have
@@ -737,7 +737,8 @@ module ActiveRecord
# to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
subselect.distinct unless select.limit || select.offset || select.orders.any?
- Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key.name))
+ key_name = quote_column_name(key.name)
+ Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name))
end
def supports_rename_index?
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 16273fb5f1..5d81de9fe1 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -11,11 +11,11 @@ module ActiveRecord
# Instantiates a new column in the table.
#
- # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int</tt>.
+ # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id bigint</tt>.
# +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, table_name = nil, default_function = nil, collation = nil, comment: 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
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 29542f917e..901717ae3d 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -156,7 +156,6 @@ module ActiveRecord
env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url"))
end
- config.reject! { |k, v| v.is_a?(Hash) && !(v.key?("adapter") || v.key?("url")) }
config.merge! env_config if env_config
config.each do |key, value|
@@ -183,13 +182,25 @@ module ActiveRecord
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
+ # Require the adapter itself and give useful feedback about
+ # 1. Missing adapter gems and
+ # 2. Adapter gems' missing dependencies.
path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter"
begin
require path_to_adapter
- rescue Gem::LoadError => e
- raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)."
rescue LoadError => e
- raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
+ # We couldn't require the adapter itself. Raise an exception that
+ # points out config typos and missing gems.
+ if e.path == path_to_adapter
+ # We can assume that a non-builtin adapter was specified, so it's
+ # either misspelled or missing from Gemfile.
+ raise e.class, "Could not load the '#{spec[:adapter]}' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
+
+ # Bubbled up from the adapter require. Prefix the exception message
+ # with some guidance about how to address it and reraise.
+ else
+ raise e.class, "Error loading the '#{spec[:adapter]}' Active Record adapter. Missing a gem it depends on? #{e.message}", e.backtrace
+ end
end
adapter_method = "#{spec[:adapter]}_connection"
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
index a058a72872..4106ce01be 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -11,7 +11,7 @@ module ActiveRecord
else
super
end
- @connection.next_result while @connection.more_results?
+ discard_remaining_results
result
end
@@ -50,11 +50,18 @@ module ActiveRecord
alias :exec_update :exec_delete
private
+ def default_insert_value(column)
+ Arel.sql("DEFAULT") unless column.auto_increment?
+ end
def last_inserted_id(result)
@connection.last_id
end
+ def discard_remaining_results
+ @connection.abandon_results!
+ end
+
def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
# made since we established the connection
@@ -64,10 +71,7 @@ module ActiveRecord
log(sql, name, binds, type_casted_binds) do
if cache_stmt
- cache = @statements[sql] ||= {
- stmt: @connection.prepare(sql)
- }
- stmt = cache[:stmt]
+ stmt = @statements[sql] ||= @connection.prepare(sql)
else
stmt = @connection.prepare(sql)
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 75377693c6..c9ea653b77 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
@@ -4,8 +4,7 @@ module ActiveRecord
module ConnectionAdapters
module MySQL
class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
- delegate :add_sql_comment!, :mariadb?, to: :@conn
- private :add_sql_comment!, :mariadb?
+ delegate :add_sql_comment!, :mariadb?, to: :@conn, private: true
private
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
index b22a2e4da7..2ed4ad16ae 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
@@ -4,11 +4,6 @@ module ActiveRecord
module ConnectionAdapters
module MySQL
module ColumnMethods
- def primary_key(name, type = :primary_key, **options)
- options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default)
- super
- end
-
def blob(*args, **options)
args.each { |name| column(name, :blob, options) }
end
@@ -37,10 +32,6 @@ module ActiveRecord
args.each { |name| column(name, :longtext, options) }
end
- def json(*args, **options)
- args.each { |name| column(name, :json, options) }
- end
-
def unsigned_integer(*args, **options)
args.each { |name| column(name, :unsigned_integer, options) }
end
@@ -68,7 +59,6 @@ module ActiveRecord
when :primary_key
type = :integer
options[:limit] ||= 8
- options[:auto_increment] = true
options[:primary_key] = true
when /\Aunsigned_(?<type>.+)\z/
type = $~[:type].to_sym
@@ -82,6 +72,11 @@ module ActiveRecord
def aliased_types(name, fallback)
fallback
end
+
+ def integer_like_primary_key_type(type, options)
+ options[:auto_increment] = true
+ type
+ end
end
class Table < ActiveRecord::ConnectionAdapters::Table
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 95eb77aea4..d23178e43c 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -8,6 +8,7 @@ module ActiveRecord
def prepare_column_options(column)
spec = super
spec[:unsigned] = "true" if column.unsigned?
+ spec[:auto_increment] = "true" if column.auto_increment?
if @connection.supports_virtual_columns? && column.virtual?
spec[:as] = extract_expression_for_virtual_column(column)
@@ -18,6 +19,12 @@ module ActiveRecord
spec
end
+ def column_spec_for_primary_key(column)
+ spec = super
+ spec.delete(:auto_increment) if column.type == :integer && column.auto_increment?
+ spec
+ end
+
def default_primary_key?(column)
super && column.auto_increment? && !column.unsigned?
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
index 759493e3bd..ce50590651 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -5,13 +5,7 @@ module ActiveRecord
module MySQL
module SchemaStatements # :nodoc:
# Returns an array of indexes for the given table.
- def indexes(table_name, name = nil)
- if name
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing name to #indexes is deprecated without replacement.
- MSG
- end
-
+ def indexes(table_name)
indexes = []
current_index = nil
execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
@@ -28,23 +22,26 @@ module ActiveRecord
index_using = mysql_index_type
end
- indexes << IndexDefinition.new(
+ indexes << [
row[:Table],
row[:Key_name],
row[:Non_unique].to_i == 0,
+ [],
+ lengths: {},
+ orders: {},
type: index_type,
using: index_using,
comment: row[:Index_comment].presence
- )
+ ]
end
- indexes.last.columns << row[:Column_name]
- indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part]
- indexes.last.orders.merge!(row[:Column_name] => :desc) if row[:Collation] == "D"
+ indexes.last[-2] << row[:Column_name]
+ indexes.last[-1][:lengths].merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part]
+ indexes.last[-1][:orders].merge!(row[:Column_name] => :desc) if row[:Collation] == "D"
end
end
- indexes
+ indexes.map { |index| IndexDefinition.new(*index) }
end
def remove_column(table_name, column_name, type = nil, options = {})
@@ -109,6 +106,18 @@ module ActiveRecord
super unless specifier == "RESTRICT"
end
+ def add_index_length(quoted_columns, **options)
+ lengths = options_for_index_columns(options[:length])
+ quoted_columns.each do |name, column|
+ column << "(#{lengths[name]})" if lengths[name].present?
+ end
+ end
+
+ def add_options_for_index_columns(quoted_columns, **options)
+ quoted_columns = add_index_length(quoted_columns, options)
+ super
+ end
+
def data_source_sql(name = nil, type: nil)
scope = quoted_scope(name, type: type)
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 2c2321872d..4c57bd48ab 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,11 +1,10 @@
# frozen_string_literal: true
-require_relative "abstract_mysql_adapter"
-require_relative "mysql/database_statements"
+require "active_record/connection_adapters/abstract_mysql_adapter"
+require "active_record/connection_adapters/mysql/database_statements"
-gem "mysql2", ">= 0.3.18", "< 0.5"
+gem "mysql2", ">= 0.4.4", "< 0.6.0"
require "mysql2"
-raise "mysql2 0.4.3 is not supported. Please upgrade to 0.4.4+" if Mysql2::VERSION == "0.4.3"
module ActiveRecord
module ConnectionHandling # :nodoc:
@@ -105,6 +104,11 @@ module ActiveRecord
@connection.close
end
+ def discard! # :nodoc:
+ @connection.automatic_close = false
+ @connection = nil
+ end
+
private
def connect
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index ff95fa4a0e..3ccc7271ab 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -7,6 +7,11 @@ module ActiveRecord
delegate :array, :oid, :fmod, to: :sql_type_metadata
alias :array? :array
+ def initialize(*, max_identifier_length: 63, **)
+ super
+ @max_identifier_length = max_identifier_length
+ end
+
def serial?
return unless default_function
@@ -16,7 +21,21 @@ module ActiveRecord
end
private
+ attr_reader :max_identifier_length
+
def sequence_name_from_parts(table_name, column_name, suffix)
+ over_length = [table_name, column_name, suffix].map(&:length).sum + 2 - max_identifier_length
+
+ if over_length > 0
+ column_name_length = [(max_identifier_length - suffix.length - 2) / 2, column_name.length].min
+ over_length -= column_name.length - column_name_length
+ column_name = column_name[0, column_name_length - [over_length, 0].min]
+ end
+
+ if over_length > 0
+ table_name = table_name[0, table_name.length - over_length]
+ end
+
"#{table_name}_#{column_name}_#{suffix}"
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index b28418d74f..247a25054e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -1,27 +1,28 @@
# frozen_string_literal: true
-require_relative "oid/array"
-require_relative "oid/bit"
-require_relative "oid/bit_varying"
-require_relative "oid/bytea"
-require_relative "oid/cidr"
-require_relative "oid/date_time"
-require_relative "oid/decimal"
-require_relative "oid/enum"
-require_relative "oid/hstore"
-require_relative "oid/inet"
-require_relative "oid/jsonb"
-require_relative "oid/money"
-require_relative "oid/oid"
-require_relative "oid/point"
-require_relative "oid/legacy_point"
-require_relative "oid/range"
-require_relative "oid/specialized_string"
-require_relative "oid/uuid"
-require_relative "oid/vector"
-require_relative "oid/xml"
+require "active_record/connection_adapters/postgresql/oid/array"
+require "active_record/connection_adapters/postgresql/oid/bit"
+require "active_record/connection_adapters/postgresql/oid/bit_varying"
+require "active_record/connection_adapters/postgresql/oid/bytea"
+require "active_record/connection_adapters/postgresql/oid/cidr"
+require "active_record/connection_adapters/postgresql/oid/date"
+require "active_record/connection_adapters/postgresql/oid/date_time"
+require "active_record/connection_adapters/postgresql/oid/decimal"
+require "active_record/connection_adapters/postgresql/oid/enum"
+require "active_record/connection_adapters/postgresql/oid/hstore"
+require "active_record/connection_adapters/postgresql/oid/inet"
+require "active_record/connection_adapters/postgresql/oid/jsonb"
+require "active_record/connection_adapters/postgresql/oid/money"
+require "active_record/connection_adapters/postgresql/oid/oid"
+require "active_record/connection_adapters/postgresql/oid/point"
+require "active_record/connection_adapters/postgresql/oid/legacy_point"
+require "active_record/connection_adapters/postgresql/oid/range"
+require "active_record/connection_adapters/postgresql/oid/specialized_string"
+require "active_record/connection_adapters/postgresql/oid/uuid"
+require "active_record/connection_adapters/postgresql/oid/vector"
+require "active_record/connection_adapters/postgresql/oid/xml"
-require_relative "oid/type_map_initializer"
+require "active_record/connection_adapters/postgresql/oid/type_map_initializer"
module ActiveRecord
module ConnectionAdapters
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
index 587e95d192..e9a79526f9 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -43,10 +43,7 @@ module ActiveRecord
/\A[0-9A-F]*\Z/i.match?(value)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :value
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
new file mode 100644
index 0000000000..24a1daa95a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Date < Type::Date # :nodoc:
+ def cast_value(value)
+ case value
+ when "infinity" then ::Float::INFINITY
+ when "-infinity" then -::Float::INFINITY
+ when / BC$/
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
index 879dba7afd..e7d33855c4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
@@ -6,7 +6,7 @@ module ActiveRecord
module OID # :nodoc:
class Decimal < Type::Decimal # :nodoc:
def infinity(options = {})
- BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
+ BigDecimal("Infinity") * (options[:negative] ? -1 : 1)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
index 7d5d7d91e6..6edb7cfd3c 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -35,7 +35,7 @@ module ActiveRecord
if value.is_a?(::Range)
from = type_cast_single_for_database(value.begin)
to = type_cast_single_for_database(value.end)
- "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}"
+ ::Range.new(from, to, value.exclude_end?)
else
super
end
@@ -60,7 +60,7 @@ module ActiveRecord
end
def type_cast_single_for_database(value)
- infinity?(value) ? "" : @subtype.serialize(value)
+ infinity?(value) ? value : @subtype.serialize(value)
end
def extract_bounds(value)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index a0a22ba0f1..e75202b0be 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -101,6 +101,8 @@ module ActiveRecord
end
when OID::Array::Data
_quote(encode_array(value))
+ when Range
+ _quote(encode_range(value))
else
super
end
@@ -117,6 +119,8 @@ module ActiveRecord
value.to_s
when OID::Array::Data
encode_array(value)
+ when Range
+ encode_range(value)
else
super
end
@@ -133,6 +137,10 @@ module ActiveRecord
result
end
+ def encode_range(range)
+ "[#{type_cast_range_value(range.first)},#{type_cast_range_value(range.last)}#{range.exclude_end? ? ')' : ']'}"
+ end
+
def determine_encoding_of_strings_in_array(value)
case value
when ::Array then determine_encoding_of_strings_in_array(value.first)
@@ -146,6 +154,14 @@ module ActiveRecord
else _type_cast(values)
end
end
+
+ def type_cast_range_value(value)
+ infinity?(value) ? "" : type_cast(value)
+ end
+
+ def infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
index 386d22a9bd..8df91c988b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -4,26 +4,21 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module ReferentialIntegrity # :nodoc:
- def supports_disable_referential_integrity? # :nodoc:
- true
- end
-
def disable_referential_integrity # :nodoc:
- if supports_disable_referential_integrity?
- original_exception = nil
+ original_exception = nil
- begin
- transaction(requires_new: true) do
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
- end
- rescue ActiveRecord::ActiveRecordError => e
- original_exception = e
+ begin
+ transaction(requires_new: true) do
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
end
+ rescue ActiveRecord::ActiveRecordError => e
+ original_exception = e
+ end
- begin
- yield
- rescue ActiveRecord::InvalidForeignKey => e
- warn <<-WARNING
+ begin
+ yield
+ rescue ActiveRecord::InvalidForeignKey => e
+ warn <<-WARNING
WARNING: Rails was not able to disable referential integrity.
This is most likely caused due to missing permissions.
@@ -32,17 +27,14 @@ Rails needs superuser privileges to disable referential integrity.
cause: #{original_exception.try(:message)}
WARNING
- raise e
- end
+ raise e
+ end
- begin
- transaction(requires_new: true) do
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
- end
- rescue ActiveRecord::ActiveRecordError
+ begin
+ transaction(requires_new: true) do
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
end
- else
- yield
+ rescue ActiveRecord::ActiveRecordError
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
index 59f661da25..8e381a92cf 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
@@ -5,6 +5,18 @@ module ActiveRecord
module PostgreSQL
class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
private
+ def visit_AlterTable(o)
+ super << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ")
+ end
+
+ def visit_AddForeignKey(o)
+ super.dup.tap { |sql| sql << " NOT VALID" unless o.validate? }
+ end
+
+ def visit_ValidateConstraint(name)
+ "VALIDATE CONSTRAINT #{quote_column_name(name)}"
+ end
+
def add_column_options!(sql, options)
if options[:collation]
sql << " COLLATE \"#{options[:collation]}\""
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
index f1489e4d69..6047217fcd 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -44,15 +44,8 @@ module ActiveRecord
# a record (as primary keys cannot be +nil+). This might be done via the
# +SecureRandom.uuid+ method and a +before_save+ callback, for instance.
def primary_key(name, type = :primary_key, **options)
- options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default)
if type == :uuid
options[:default] = options.fetch(:default, "gen_random_uuid()")
- elsif options.delete(:auto_increment) == true && %i(integer bigint).include?(type)
- type = if type == :bigint || options[:limit] == 8
- :bigserial
- else
- :serial
- end
end
super
@@ -102,10 +95,6 @@ module ActiveRecord
args.each { |name| column(name, :int8range, options) }
end
- def json(*args, **options)
- args.each { |name| column(name, :json, options) }
- end
-
def jsonb(*args, **options)
args.each { |name| column(name, :jsonb, options) }
end
@@ -185,11 +174,33 @@ module ActiveRecord
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
include ColumnMethods
+
+ private
+ def integer_like_primary_key_type(type, options)
+ if type == :bigint || options[:limit] == 8
+ :bigserial
+ else
+ :serial
+ end
+ end
end
class Table < ActiveRecord::ConnectionAdapters::Table
include ColumnMethods
end
+
+ class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable
+ attr_reader :constraint_validations
+
+ def initialize(td)
+ super
+ @constraint_validations = []
+ end
+
+ def validate_constraint(name)
+ @constraint_validations << name
+ end
+ end
end
end
end
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 c0dbb166b7..84643d20da 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
@@ -5,6 +5,18 @@ module ActiveRecord
module PostgreSQL
class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
private
+
+ def extensions(stream)
+ extensions = @connection.extensions
+ if extensions.any?
+ stream.puts " # These are extensions that must be enabled in order to support this database"
+ extensions.sort.each do |extension|
+ stream.puts " enable_extension #{extension.inspect}"
+ end
+ stream.puts
+ end
+ end
+
def prepare_column_options(column)
spec = super
spec[:array] = "true" if column.array?
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 9e2f61e6ce..e20e5f2914 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/string/strip"
-
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -40,7 +38,7 @@ module ActiveRecord
" TABLESPACE = \"#{value}\""
when :connection_limit
" CONNECTION LIMIT = #{value}"
- else
+ else
""
end
end
@@ -66,12 +64,7 @@ module ActiveRecord
end
# Verifies existence of an index with a given name.
- def index_name_exists?(table_name, index_name, default = nil)
- unless default.nil?
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing default to #index_name_exists? is deprecated without replacement.
- MSG
- end
+ def index_name_exists?(table_name, index_name)
table = quoted_scope(table_name)
index = quoted_scope(index_name)
@@ -89,21 +82,12 @@ module ActiveRecord
end
# Returns an array of indexes for the given table.
- def indexes(table_name, name = nil) # :nodoc:
- if name
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing name to #indexes is deprecated without replacement.
- MSG
- end
-
+ def indexes(table_name) # :nodoc:
scope = quoted_scope(table_name)
result = query(<<-SQL, "SCHEMA")
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,
- (SELECT COUNT(*) FROM pg_opclass o
- JOIN (SELECT unnest(string_to_array(d.indclass::text, ' '))::int oid) c
- ON o.oid = c.oid WHERE o.opcdefault = 'f')
+ 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
@@ -122,24 +106,32 @@ module ActiveRecord
inddef = row[3]
oid = row[4]
comment = row[5]
- opclass = row[6]
- using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten
+ using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/m).flatten
- if indkey.include?(0) || opclass > 0
+ orders = {}
+ opclasses = {}
+
+ if indkey.include?(0)
columns = expressions
else
- columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact
+ columns = Hash[query(<<~SQL, "SCHEMA")].values_at(*indkey).compact
SELECT a.attnum, a.attname
FROM pg_attribute a
WHERE a.attrelid = #{oid}
AND a.attnum IN (#{indkey.join(",")})
SQL
- # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
- orders = Hash[
- expressions.scan(/(\w+) DESC/).flatten.map { |order_column| [order_column, :desc] }
- ]
+ # add info on sort order (only desc order is explicitly specified, asc is the default)
+ # and non-default opclasses
+ expressions.scan(/(?<column>\w+)\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
+ opclasses[column] = opclass.to_sym if opclass
+ if nulls
+ orders[column] = [desc, nulls].compact.join(" ")
+ else
+ orders[column] = :desc if desc
+ end
+ end
end
IndexDefinition.new(
@@ -148,6 +140,7 @@ module ActiveRecord
unique,
columns,
orders: orders,
+ opclasses: opclasses,
where: where,
using: using.to_sym,
comment: comment.presence
@@ -165,7 +158,7 @@ module ActiveRecord
def table_comment(table_name) # :nodoc:
scope = quoted_scope(table_name, type: "BASE TABLE")
if scope[:name]
- query_value(<<-SQL.strip_heredoc, "SCHEMA")
+ query_value(<<~SQL, "SCHEMA")
SELECT pg_catalog.obj_description(c.oid, 'pg_class')
FROM pg_catalog.pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
@@ -360,7 +353,7 @@ module ActiveRecord
end
def primary_keys(table_name) # :nodoc:
- query_values(<<-SQL.strip_heredoc, "SCHEMA")
+ query_values(<<~SQL, "SCHEMA")
SELECT a.attname
FROM (
SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx
@@ -375,6 +368,31 @@ module ActiveRecord
SQL
end
+ def bulk_change_table(table_name, operations)
+ sql_fragments = []
+ non_combinable_operations = []
+
+ operations.each do |command, args|
+ table, arguments = args.shift, args
+ method = :"#{command}_for_alter"
+
+ if respond_to?(method, true)
+ sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
+ sql_fragments << sqls
+ non_combinable_operations.concat(procs)
+ else
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+ non_combinable_operations.each(&:call)
+ sql_fragments = []
+ non_combinable_operations = []
+ send(command, table, *arguments)
+ end
+ end
+
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+ non_combinable_operations.each(&:call)
+ end
+
# Renames a table.
# Also renames a table's primary key sequence if the sequence name exists and
# matches the Active Record default.
@@ -405,50 +423,23 @@ module ActiveRecord
def change_column(table_name, column_name, type, options = {}) #:nodoc:
clear_cache!
- quoted_table_name = quote_table_name(table_name)
- quoted_column_name = quote_column_name(column_name)
- sql_type = type_to_sql(type, options)
- sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup
- if options[:collation]
- sql << " COLLATE \"#{options[:collation]}\""
- end
- if options[:using]
- sql << " USING #{options[:using]}"
- elsif options[:cast_as]
- cast_as_type = type_to_sql(options[:cast_as], options)
- sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})"
- end
- execute sql
-
- change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
- change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
- change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
+ sqls, procs = Array(change_column_for_alter(table_name, column_name, type, options)).partition { |v| v.is_a?(String) }
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sqls.join(", ")}"
+ procs.each(&:call)
end
# Changes the default value of a table column.
def change_column_default(table_name, column_name, default_or_changes) # :nodoc:
- clear_cache!
- column = column_for(table_name, column_name)
- return unless column
-
- default = extract_new_default_value(default_or_changes)
- alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s"
- if default.nil?
- # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
- # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
- execute alter_column_query % "DROP DEFAULT"
- else
- execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}"
- end
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_default_for_alter(table_name, column_name, default_or_changes)}"
end
def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
clear_cache!
unless null || default.nil?
column = column_for(table_name, column_name)
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column
+ execute "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL" if column
end
- execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_null_for_alter(table_name, column_name, null, default)}"
end
# Adds comment for given table column or drops it if +comment+ is a +nil+
@@ -471,8 +462,8 @@ module ActiveRecord
end
def add_index(table_name, column_name, options = {}) #:nodoc:
- 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
+ index_name, index_type, index_columns_and_opclasses, 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_and_opclasses})#{index_options}").tap do
execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment
end
end
@@ -511,8 +502,8 @@ module ActiveRecord
def foreign_keys(table_name)
scope = quoted_scope(table_name)
- fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA")
- SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
+ fk_info = exec_query(<<~SQL, "SCHEMA")
+ SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid
FROM pg_constraint c
JOIN pg_class t1 ON c.conrelid = t1.oid
JOIN pg_class t2 ON c.confrelid = t2.oid
@@ -534,11 +525,20 @@ module ActiveRecord
options[:on_delete] = extract_foreign_key_action(row["on_delete"])
options[:on_update] = extract_foreign_key_action(row["on_update"])
+ options[:validate] = row["valid"]
ForeignKeyDefinition.new(table_name, row["to_table"], options)
end
end
+ def foreign_tables
+ query_values(data_source_sql(type: "FOREIGN TABLE"), "SCHEMA")
+ end
+
+ def foreign_table_exists?(table_name)
+ query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present?
+ end
+
# Maps logical Rails types to PostgreSQL-specific data types.
def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc:
sql = \
@@ -583,7 +583,7 @@ module ActiveRecord
.gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
- [super, *order_columns].join(", ")
+ (order_columns << super).join(", ")
end
def update_table_definition(table_name, base) # :nodoc:
@@ -594,6 +594,43 @@ module ActiveRecord
PostgreSQL::SchemaDumper.create(self, options)
end
+ # Validates the given constraint.
+ #
+ # Validates the constraint named +constraint_name+ on +accounts+.
+ #
+ # validate_constraint :accounts, :constraint_name
+ def validate_constraint(table_name, constraint_name)
+ return unless supports_validate_constraints?
+
+ at = create_alter_table table_name
+ at.validate_constraint constraint_name
+
+ execute schema_creation.accept(at)
+ end
+
+ # Validates the given foreign key.
+ #
+ # Validates the foreign key on +accounts.branch_id+.
+ #
+ # validate_foreign_key :accounts, :branches
+ #
+ # Validates the foreign key on +accounts.owner_id+.
+ #
+ # validate_foreign_key :accounts, column: :owner_id
+ #
+ # Validates the foreign key named +special_fk_name+ on the +accounts+ table.
+ #
+ # validate_foreign_key :accounts, name: :special_fk_name
+ #
+ # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key.
+ def validate_foreign_key(from_table, options_or_to_table = {})
+ return unless supports_validate_constraints?
+
+ fk_name_to_validate = foreign_key_for!(from_table, options_or_to_table).name
+
+ validate_constraint from_table, fk_name_to_validate
+ end
+
private
def schema_creation
PostgreSQL::SchemaCreation.new(self)
@@ -603,6 +640,10 @@ module ActiveRecord
PostgreSQL::TableDefinition.new(*args)
end
+ def create_alter_table(name)
+ PostgreSQL::AlterTable.new create_table_definition(name)
+ end
+
def new_column_from_field(table_name, field)
column_name, type, default, notnull, oid, fmod, collation, comment = field
type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i)
@@ -617,7 +658,8 @@ module ActiveRecord
table_name,
default_function,
collation,
- comment: comment.presence
+ comment: comment.presence,
+ max_identifier_length: max_identifier_length
)
end
@@ -641,9 +683,75 @@ module ActiveRecord
end
end
+ def change_column_sql(table_name, column_name, type, options = {})
+ quoted_column_name = quote_column_name(column_name)
+ sql_type = type_to_sql(type, options)
+ sql = "ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup
+ if options[:collation]
+ sql << " COLLATE \"#{options[:collation]}\""
+ end
+ if options[:using]
+ sql << " USING #{options[:using]}"
+ elsif options[:cast_as]
+ cast_as_type = type_to_sql(options[:cast_as], options)
+ sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})"
+ end
+
+ sql
+ end
+
+ def change_column_for_alter(table_name, column_name, type, options = {})
+ sqls = [change_column_sql(table_name, column_name, type, options)]
+ sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default)
+ sqls << change_column_null_for_alter(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment)
+ sqls
+ end
+
+
+ # Changes the default value of a table column.
+ def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc:
+ column = column_for(table_name, column_name)
+ return unless column
+
+ default = extract_new_default_value(default_or_changes)
+ alter_column_query = "ALTER COLUMN #{quote_column_name(column_name)} %s"
+ if default.nil?
+ # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
+ # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
+ alter_column_query % "DROP DEFAULT"
+ else
+ alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}"
+ end
+ end
+
+ def change_column_null_for_alter(table_name, column_name, null, default = nil) #:nodoc:
+ "ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
+ end
+
+ def add_timestamps_for_alter(table_name, options = {})
+ [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
+ end
+
+ def remove_timestamps_for_alter(table_name, options = {})
+ [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)]
+ end
+
+ def add_index_opclass(quoted_columns, **options)
+ opclasses = options_for_index_columns(options[:opclass])
+ quoted_columns.each do |name, column|
+ column << " #{opclasses[name]}" if opclasses[name].present?
+ end
+ end
+
+ def add_options_for_index_columns(quoted_columns, **options)
+ quoted_columns = add_index_opclass(quoted_columns, options)
+ super
+ end
+
def data_source_sql(name = nil, type: nil)
scope = quoted_scope(name, type: type)
- scope[:type] ||= "'r','v','m'" # (r)elation/table, (v)iew, (m)aterialized view
+ scope[:type] ||= "'r','v','m','f'" # (r)elation/table, (v)iew, (m)aterialized view, (f)oreign table
sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace".dup
sql << " WHERE n.nspname = #{scope[:schema]}"
@@ -660,6 +768,8 @@ module ActiveRecord
"'r'"
when "VIEW"
"'v','m'"
+ when "FOREIGN TABLE"
+ "'f'"
end
scope = {}
scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))"
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 4d37a292d6..fdf6f75108 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1,23 +1,23 @@
# frozen_string_literal: true
# Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility
-gem "pg", "~> 0.18"
+gem "pg", ">= 0.18", "< 2.0"
require "pg"
-require_relative "abstract_adapter"
-require_relative "statement_pool"
-require_relative "postgresql/column"
-require_relative "postgresql/database_statements"
-require_relative "postgresql/explain_pretty_printer"
-require_relative "postgresql/oid"
-require_relative "postgresql/quoting"
-require_relative "postgresql/referential_integrity"
-require_relative "postgresql/schema_creation"
-require_relative "postgresql/schema_definitions"
-require_relative "postgresql/schema_dumper"
-require_relative "postgresql/schema_statements"
-require_relative "postgresql/type_metadata"
-require_relative "postgresql/utils"
+require "active_record/connection_adapters/abstract_adapter"
+require "active_record/connection_adapters/statement_pool"
+require "active_record/connection_adapters/postgresql/column"
+require "active_record/connection_adapters/postgresql/database_statements"
+require "active_record/connection_adapters/postgresql/explain_pretty_printer"
+require "active_record/connection_adapters/postgresql/oid"
+require "active_record/connection_adapters/postgresql/quoting"
+require "active_record/connection_adapters/postgresql/referential_integrity"
+require "active_record/connection_adapters/postgresql/schema_creation"
+require "active_record/connection_adapters/postgresql/schema_definitions"
+require "active_record/connection_adapters/postgresql/schema_dumper"
+require "active_record/connection_adapters/postgresql/schema_statements"
+require "active_record/connection_adapters/postgresql/type_metadata"
+require "active_record/connection_adapters/postgresql/utils"
module ActiveRecord
module ConnectionHandling # :nodoc:
@@ -122,6 +122,10 @@ module ActiveRecord
include PostgreSQL::SchemaStatements
include PostgreSQL::DatabaseStatements
+ def supports_bulk_alter?
+ true
+ end
+
def supports_index_sort_order?
true
end
@@ -142,6 +146,10 @@ module ActiveRecord
true
end
+ def supports_validate_constraints?
+ true
+ end
+
def supports_views?
true
end
@@ -166,7 +174,7 @@ module ActiveRecord
{ concurrently: "CONCURRENTLY" }
end
- class StatementPool < ConnectionAdapters::StatementPool
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
def initialize(connection, max)
super(max)
@connection = connection
@@ -182,7 +190,6 @@ module ActiveRecord
end
private
-
def dealloc(key)
@connection.query "DEALLOCATE #{key}" if connection_active?
rescue PG::Error
@@ -273,6 +280,11 @@ module ActiveRecord
end
end
+ def discard! # :nodoc:
+ @connection.socket_io.reopen(IO::NULL) rescue nil
+ @connection = nil
+ end
+
def native_database_types #:nodoc:
NATIVE_DATABASE_TYPES
end
@@ -306,20 +318,24 @@ module ActiveRecord
postgresql_version >= 90300
end
+ def supports_foreign_tables?
+ postgresql_version >= 90300
+ end
+
def supports_pgcrypto_uuid?
postgresql_version >= 90400
end
def get_advisory_lock(lock_id) # :nodoc:
unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63
- raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer")
+ raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer")
end
query_value("SELECT pg_try_advisory_lock(#{lock_id})")
end
def release_advisory_lock(lock_id) # :nodoc:
unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63
- raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer")
+ raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer")
end
query_value("SELECT pg_advisory_unlock(#{lock_id})")
end
@@ -337,25 +353,20 @@ module ActiveRecord
end
def extension_enabled?(name)
- if supports_extensions?
- res = exec_query("SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", "SCHEMA")
- res.cast_values.first
- end
+ res = exec_query("SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", "SCHEMA")
+ res.cast_values.first
end
def extensions
- if supports_extensions?
- exec_query("SELECT extname FROM pg_extension", "SCHEMA").cast_values
- else
- super
- end
+ exec_query("SELECT extname FROM pg_extension", "SCHEMA").cast_values
end
# Returns the configured supported identifier length supported by PostgreSQL
- def table_alias_length
+ def max_identifier_length
@max_identifier_length ||= query_value("SHOW max_identifier_length", "SCHEMA").to_i
end
- alias index_name_length table_alias_length
+ alias table_alias_length max_identifier_length
+ alias index_name_length max_identifier_length
# Set the authorized user for this session
def session_auth=(user)
@@ -387,7 +398,6 @@ module ActiveRecord
end
private
-
# See https://www.postgresql.org/docs/current/static/errcodes-appendix.html
VALUE_LIMIT_VIOLATION = "22001"
NUMERIC_VALUE_OUT_OF_RANGE = "22003"
@@ -396,6 +406,8 @@ module ActiveRecord
UNIQUE_VIOLATION = "23505"
SERIALIZATION_FAILURE = "40001"
DEADLOCK_DETECTED = "40P01"
+ LOCK_NOT_AVAILABLE = "55P03"
+ QUERY_CANCELED = "57014"
def translate_exception(exception, message)
return exception unless exception.respond_to?(:result)
@@ -415,6 +427,10 @@ module ActiveRecord
SerializationFailure.new(message)
when DEADLOCK_DETECTED
Deadlocked.new(message)
+ when LOCK_NOT_AVAILABLE
+ LockWaitTimeout.new(message)
+ when QUERY_CANCELED
+ QueryCanceled.new(message)
else
super
end
@@ -449,7 +465,7 @@ module ActiveRecord
register_class_with_limit m, "bit", OID::Bit
register_class_with_limit m, "varbit", OID::BitVarying
m.alias_type "timestamptz", "timestamp"
- m.register_type "date", Type::Date.new
+ m.register_type "date", OID::Date.new
m.register_type "money", OID::Money.new
m.register_type "bytea", OID::Bytea.new
@@ -821,6 +837,7 @@ module ActiveRecord
ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql)
ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql)
ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql)
+ ActiveRecord::Type.register(:date, OID::Date, adapter: :postgresql)
ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :postgresql)
ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql)
ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql)
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index f34b6733da..c29cf1f9a1 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -28,7 +28,7 @@ module ActiveRecord
coder["columns_hash"] = @columns_hash
coder["primary_keys"] = @primary_keys
coder["data_sources"] = @data_sources
- coder["version"] = ActiveRecord::Migrator.current_version
+ coder["version"] = connection.migration_context.current_version
end
def init_with(coder)
@@ -100,7 +100,7 @@ module ActiveRecord
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
- @version = ActiveRecord::Migrator.current_version
+ @version = connection.migration_context.current_version
[@version, @columns, @columns_hash, @primary_keys, @data_sources]
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
index 8042dbfea2..70de96326c 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -17,7 +17,7 @@ module ActiveRecord
end
def quoted_time(value)
- quoted_date(value)
+ quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ")
end
def quoted_binary(value)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb
index 501f17dbad..c9855019c1 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb
@@ -3,27 +3,16 @@
module ActiveRecord
module ConnectionAdapters
module SQLite3
- module ColumnMethods
- def primary_key(name, type = :primary_key, **options)
- if %i(integer bigint).include?(type) && (options.delete(:auto_increment) == true || !options.key?(:default))
- type = :primary_key
- end
-
- super
- end
- end
-
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
- include ColumnMethods
-
def references(*args, **options)
super(*args, type: :integer, **options)
end
alias :belongs_to :references
- end
- class Table < ActiveRecord::ConnectionAdapters::Table
- include ColumnMethods
+ private
+ def integer_like_primary_key_type(type, options)
+ :primary_key
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
index a512702b7b..58e5138e02 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -5,13 +5,7 @@ module ActiveRecord
module SQLite3
module SchemaStatements # :nodoc:
# Returns an array of indexes for the given table.
- def indexes(table_name, name = nil)
- if name
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing name to #indexes is deprecated without replacement.
- MSG
- end
-
+ def indexes(table_name)
exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row|
index_sql = query_value(<<-SQL, "SCHEMA")
SELECT sql
@@ -29,20 +23,26 @@ module ActiveRecord
col["name"]
end
+ # Add info on sort order for columns (only desc order is explicitly specified, asc is
+ # the default)
+ orders = {}
+ if index_sql # index_sql can be null in case of primary key indexes
+ index_sql.scan(/"(\w+)" DESC/).flatten.each { |order_column|
+ orders[order_column] = :desc
+ }
+ end
+
IndexDefinition.new(
table_name,
row["name"],
row["unique"] != 0,
columns,
- where: where
+ where: where,
+ orders: orders
)
end
end
- def update_table_definition(table_name, base)
- SQLite3::Table.new(table_name, base)
- end
-
def create_schema_dumper(options)
SQLite3::SchemaDumper.create(self, options)
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 6fdd666486..800e731f06 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
-require_relative "abstract_adapter"
-require_relative "statement_pool"
-require_relative "sqlite3/explain_pretty_printer"
-require_relative "sqlite3/quoting"
-require_relative "sqlite3/schema_creation"
-require_relative "sqlite3/schema_definitions"
-require_relative "sqlite3/schema_dumper"
-require_relative "sqlite3/schema_statements"
+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"
+require "active_record/connection_adapters/sqlite3/schema_definitions"
+require "active_record/connection_adapters/sqlite3/schema_dumper"
+require "active_record/connection_adapters/sqlite3/schema_statements"
gem "sqlite3", "~> 1.3.6"
require "sqlite3"
@@ -60,7 +60,7 @@ module ActiveRecord
include SQLite3::SchemaStatements
NATIVE_DATABASE_TYPES = {
- primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL",
+ primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL",
string: { name: "varchar" },
text: { name: "text" },
integer: { name: "integer" },
@@ -70,7 +70,8 @@ module ActiveRecord
time: { name: "time" },
date: { name: "date" },
binary: { name: "blob" },
- boolean: { name: "boolean" }
+ boolean: { name: "boolean" },
+ json: { name: "json" },
}
##
@@ -90,18 +91,17 @@ module ActiveRecord
# Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
class_attribute :represent_boolean_as_integer, default: false
- class StatementPool < ConnectionAdapters::StatementPool
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
private
-
def dealloc(stmt)
- stmt[:stmt].close unless stmt[:stmt].closed?
+ stmt.close unless stmt.closed?
end
end
def initialize(connection, logger, connection_options, config)
super(connection, logger, config)
- @active = nil
+ @active = true
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
configure_connection
@@ -135,12 +135,16 @@ module ActiveRecord
true
end
+ def supports_json?
+ true
+ end
+
def supports_multi_insert?
sqlite_version >= "3.7.11"
end
def active?
- @active != false
+ @active
end
# Disconnects from the database if already connected. Otherwise, this
@@ -220,11 +224,8 @@ module ActiveRecord
stmt.close
end
else
- cache = @statements[sql] ||= {
- stmt: @connection.prepare(sql)
- }
- stmt = cache[:stmt]
- cols = cache[:cols] ||= stmt.columns
+ stmt = @statements[sql] ||= @connection.prepare(sql)
+ cols = stmt.columns
stmt.reset!
stmt.bind_params(type_casted_binds)
records = stmt.to_a
@@ -286,19 +287,18 @@ module ActiveRecord
rename_table_indexes(table_name, new_name)
end
- # See: https://www.sqlite.org/lang_altertable.html
- # SQLite has an additional restriction on the ALTER TABLE statement
- def valid_alter_table_type?(type)
- type.to_sym != :primary_key
+ def valid_alter_table_type?(type, options = {})
+ !invalid_alter_table_type?(type, options)
end
+ deprecate :valid_alter_table_type?
def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if valid_alter_table_type?(type)
- super(table_name, column_name, type, options)
- else
+ if invalid_alter_table_type?(type, options)
alter_table(table_name) do |definition|
definition.column(column_name, type, options)
end
+ else
+ super
end
end
@@ -364,12 +364,30 @@ module ActiveRecord
end
def insert_fixtures(rows, table_name)
- rows.each do |row|
- insert_fixture(row, table_name)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `insert_fixtures` is deprecated and will be removed in the next version of Rails.
+ Consider using `insert_fixtures_set` for performance improvement.
+ MSG
+ insert_fixtures_set(table_name => rows)
+ end
+
+ def insert_fixtures_set(fixture_set, tables_to_delete = [])
+ disable_referential_integrity do
+ transaction(requires_new: true) do
+ tables_to_delete.each { |table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete" }
+
+ fixture_set.each do |table_name, rows|
+ rows.each { |row| insert_fixture(row, table_name) }
+ end
+ end
end
end
private
+ def initialize_type_map(m = type_map)
+ super
+ register_class_with_limit m, %r(int)i, SQLite3Integer
+ end
def table_structure(table_name)
structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
@@ -378,6 +396,12 @@ module ActiveRecord
end
alias column_definitions table_structure
+ # See: https://www.sqlite.org/lang_altertable.html
+ # SQLite has an additional restriction on the ALTER TABLE statement
+ def invalid_alter_table_type?(type, options)
+ type.to_sym == :primary_key || options[:primary_key]
+ end
+
def alter_table(table_name, options = {})
altered_table_name = "a#{table_name}"
caller = lambda { |definition| yield definition if block_given? }
@@ -399,18 +423,21 @@ module ActiveRecord
options[:id] = false
create_table(to, options) do |definition|
@definition = definition
- @definition.primary_key(from_primary_key) if from_primary_key.present?
+ if from_primary_key.is_a?(Array)
+ @definition.primary_keys from_primary_key
+ end
columns(from).each do |column|
column_name = options[:rename] ?
(options[:rename][column.name] ||
options[:rename][column.name.to_sym] ||
column.name) : column.name
- next if column_name == from_primary_key
@definition.column(column_name, column.type,
limit: column.limit, default: column.default,
precision: column.precision, scale: column.scale,
- null: column.null, collation: column.collation)
+ null: column.null, collation: column.collation,
+ primary_key: column_name == from_primary_key
+ )
end
yield @definition if block_given?
end
@@ -423,6 +450,9 @@ module ActiveRecord
def copy_table_indexes(from, to, rename = {})
indexes(from).each do |index|
name = index.name
+ # indexes sqlite creates for internal use start with `sqlite_` and
+ # don't need to be copied
+ next if name.starts_with?("sqlite_")
if to == "a#{from}"
name = "t#{name}"
elsif from == "a#{to}"
@@ -438,6 +468,7 @@ module ActiveRecord
# index name can't be the same
opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true }
opts[:unique] = true if index.unique
+ opts[:where] = index.where if index.where
add_index(to, columns, opts)
end
end
@@ -525,6 +556,17 @@ module ActiveRecord
def configure_connection
execute("PRAGMA foreign_keys = ON", "SCHEMA")
end
+
+ class SQLite3Integer < Type::Integer # :nodoc:
+ private
+ def _limit
+ # INTEGER storage class can be stored 8 bytes value.
+ # See https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes
+ limit || 8
+ end
+ end
+
+ ActiveRecord::Type.register(:integer, SQLite3Integer, adapter: :sqlite3)
end
ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter)
end
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 9a47edfba4..ee0e651912 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -57,6 +57,10 @@ module ActiveRecord
spec = resolver.resolve(config).symbolize_keys
spec[:name] = spec_name
+ # use the primary config if a config is not passed in and
+ # it's a three tier config
+ spec = spec[spec_name.to_sym] if spec[spec_name.to_sym]
+
connection_handler.establish_connection(spec)
end
@@ -140,6 +144,6 @@ module ActiveRecord
end
delegate :clear_active_connections!, :clear_reloadable_connections!,
- :clear_all_connections!, to: :connection_handler
+ :clear_all_connections!, :flush_idle_connections!, to: :connection_handler
end
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 945c4eca78..e1a0b2ecf8 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -18,6 +18,13 @@ module ActiveRecord
mattr_accessor :logger, instance_writer: false
##
+ # :singleton-method:
+ #
+ # Specifies if the methods calling database queries should be logged below
+ # their relevant queries. Defaults to false.
+ mattr_accessor :verbose_query_logs, instance_writer: false, default: false
+
+ ##
# Contains the database configuration - as is typically stored in config/database.yml -
# as a Hash.
#
@@ -76,25 +83,13 @@ module ActiveRecord
# scope being ignored is error-worthy, rather than a warning.
mattr_accessor :error_on_ignored_order, instance_writer: false, default: false
- def self.error_on_ignored_order_or_limit
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- The flag error_on_ignored_order_or_limit is deprecated. Limits are
- now supported. Please use error_on_ignored_order instead.
- MSG
- error_on_ignored_order
- end
-
- def error_on_ignored_order_or_limit
- self.class.error_on_ignored_order_or_limit
- end
-
- def self.error_on_ignored_order_or_limit=(value)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- The flag error_on_ignored_order_or_limit is deprecated. Limits are
- now supported. Please use error_on_ignored_order= instead.
- MSG
- self.error_on_ignored_order = value
- end
+ # :singleton-method:
+ # Specify the behavior for unsafe raw query methods. Values are as follows
+ # deprecated - Warnings are logged when unsafe raw SQL is passed to
+ # query methods.
+ # disabled - Unsafe raw SQL passed to query methods results in
+ # UnknownAttributeReference exception.
+ mattr_accessor :allow_unsafe_raw_sql, instance_writer: false, default: :deprecated
##
# :singleton-method:
@@ -286,10 +281,11 @@ module ActiveRecord
end
def relation
- relation = Relation.create(self, arel_table, predicate_builder)
+ relation = Relation.create(self)
if finder_needs_type_condition? && !ignore_default_scope?
- relation.where(type_condition).create_with(inheritance_column.to_s => sti_name)
+ relation.where!(type_condition)
+ relation.create_with!(inheritance_column.to_s => sti_name)
else
relation
end
@@ -386,8 +382,10 @@ module ActiveRecord
_run_initialize_callbacks
- @new_record = true
- @destroyed = false
+ @new_record = true
+ @destroyed = false
+ @_start_transaction_state = {}
+ @transaction_state = nil
super
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 5005d58f1c..ee4f818cbf 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -53,7 +53,7 @@ module ActiveRecord
unscoped.where(primary_key => object.id).update_all(updates)
end
- return true
+ true
end
# A generic "counter updater" implementation, intended primarily to be
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
new file mode 100644
index 0000000000..ffeed45030
--- /dev/null
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module DatabaseConfigurations # :nodoc:
+ class DatabaseConfig
+ attr_reader :env_name, :spec_name, :config
+
+ def initialize(env_name, spec_name, config)
+ @env_name = env_name
+ @spec_name = spec_name
+ @config = config
+ end
+ end
+
+ # Selects the config for the specified environment and specification name
+ #
+ # For example if passed :development, and :animals it will select the database
+ # under the :development and :animals configuration level
+ def self.config_for_env_and_spec(environment, specification_name, configs = ActiveRecord::Base.configurations) # :nodoc:
+ configs_for(environment, configs).find do |db_config|
+ db_config.spec_name == specification_name
+ end
+ end
+
+ # Collects the configs for the environment passed in.
+ #
+ # If a block is given returns the specification name and configuration
+ # otherwise returns an array of DatabaseConfig structs for the environment.
+ def self.configs_for(env, configs = ActiveRecord::Base.configurations, &blk) # :nodoc:
+ env_with_configs = db_configs(configs).select do |db_config|
+ db_config.env_name == env
+ end
+
+ if block_given?
+ env_with_configs.each do |env_with_config|
+ yield env_with_config.spec_name, env_with_config.config
+ end
+ else
+ env_with_configs
+ end
+ end
+
+ # Given an env, spec and config creates DatabaseConfig structs with
+ # each attribute set.
+ def self.walk_configs(env_name, spec_name, config) # :nodoc:
+ if config["database"] || config["url"] || config["adapter"]
+ DatabaseConfig.new(env_name, spec_name, config)
+ else
+ config.each_pair.map do |sub_spec_name, sub_config|
+ walk_configs(env_name, sub_spec_name, sub_config)
+ end
+ end
+ end
+
+ # Walks all the configs passed in and returns an array
+ # of DatabaseConfig structs for each configuration.
+ def self.db_configs(configs = ActiveRecord::Base.configurations) # :nodoc:
+ configs.each_pair.flat_map do |env_name, config|
+ walk_configs(env_name, "primary", config)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index f373b98035..23ecb24542 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -141,10 +141,7 @@ module ActiveRecord
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :name, :mapping, :subtype
end
@@ -221,6 +218,8 @@ module ActiveRecord
def detect_enum_conflict!(enum_name, method_name, klass_method = false)
if klass_method && dangerous_class_method?(method_name)
raise_conflict_error(enum_name, method_name, type: "class")
+ elsif klass_method && method_defined_within?(method_name, Relation)
+ raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name)
elsif !klass_method && dangerous_attribute_method?(method_name)
raise_conflict_error(enum_name, method_name)
elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 9ef3316393..c2a180c939 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -120,13 +120,13 @@ module ActiveRecord
def initialize(adapter = nil, message: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil)
@adapter = adapter
if table
- msg = <<-EOM.strip_heredoc
+ msg = +<<~EOM
Column `#{foreign_key}` on table `#{table}` has a type of `#{column_type(table, foreign_key)}`.
This does not match column `#{primary_key}` on `#{target_table}`, which has type `#{column_type(target_table, primary_key)}`.
To resolve this issue, change the type of the `#{foreign_key}` column on `#{table}` to be :integer. (For example `t.integer #{foreign_key}`).
EOM
else
- msg = <<-EOM
+ msg = +<<~EOM
There is a mismatch between the foreign key and primary key column types.
Verify that the foreign key column type and the primary key of the associated table match types.
EOM
@@ -335,8 +335,40 @@ module ActiveRecord
class IrreversibleOrderError < ActiveRecordError
end
- # TransactionTimeout will be raised when lock wait timeout expires.
- # Wait time value is set by innodb_lock_wait_timeout.
- class TransactionTimeout < StatementInvalid
+ # LockWaitTimeout will be raised when lock wait timeout exceeded.
+ class LockWaitTimeout < StatementInvalid
+ end
+
+ # StatementTimeout will be raised when statement timeout exceeded.
+ class StatementTimeout < StatementInvalid
+ end
+
+ # QueryCanceled will be raised when canceling statement due to user request.
+ class QueryCanceled < StatementInvalid
+ end
+
+ # UnknownAttributeReference is raised when an unknown and potentially unsafe
+ # value is passed to a query method when allow_unsafe_raw_sql is set to
+ # :disabled. For example, passing a non column name value to a relation's
+ # #order method might cause this exception.
+ #
+ # When working around this exception, caution should be taken to avoid SQL
+ # injection vulnerabilities when passing user-provided values to query
+ # methods. Known-safe values can be passed to query methods by wrapping them
+ # in Arel.sql.
+ #
+ # For example, with allow_unsafe_raw_sql set to :disabled, the following
+ # code would raise this exception:
+ #
+ # Post.order("length(title)").first
+ #
+ # The desired result can be accomplished by wrapping the known-safe string
+ # in Arel.sql:
+ #
+ # Post.order(Arel.sql("length(title)")).first
+ #
+ # Again, such a workaround should *not* be used when passing user-provided
+ # values, such as request parameters or model attributes to query methods.
+ class UnknownAttributeReference < ActiveRecordError
end
end
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index 8bb54a24b7..7ccb938888 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "explain_registry"
+require "active_record/explain_registry"
module ActiveRecord
module Explain
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index 9252fa3fed..a86217abc0 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "active_support/notifications"
-require_relative "explain_registry"
+require "active_record/explain_registry"
module ActiveRecord
class ExplainSubscriber # :nodoc:
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 12169fffa9..8f022ff7a7 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -6,8 +6,8 @@ require "zlib"
require "set"
require "active_support/dependencies"
require "active_support/core_ext/digest/uuid"
-require_relative "fixture_set/file"
-require_relative "errors"
+require "active_record/fixture_set/file"
+require "active_record/errors"
module ActiveRecord
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
@@ -169,13 +169,13 @@ module ActiveRecord
# self.use_transactional_tests = true
#
# test "godzilla" do
- # assert !Foo.all.empty?
+ # assert_not_empty Foo.all
# Foo.destroy_all
- # assert Foo.all.empty?
+ # assert_empty Foo.all
# end
#
# test "godzilla aftermath" do
- # assert !Foo.all.empty?
+ # assert_not_empty Foo.all
# end
# end
#
@@ -540,47 +540,38 @@ module ActiveRecord
}
unless files_to_read.empty?
- connection.disable_referential_integrity do
- fixtures_map = {}
-
- fixture_sets = files_to_read.map do |fs_name|
- klass = class_names[fs_name]
- conn = klass ? klass.connection : connection
- fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
- conn,
- fs_name,
- klass,
- ::File.join(fixtures_directory, fs_name))
- end
-
- update_all_loaded_fixtures fixtures_map
-
- connection.transaction(requires_new: true) do
- deleted_tables = Hash.new { |h, k| h[k] = Set.new }
- fixture_sets.each do |fs|
- conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection
- table_rows = fs.table_rows
+ fixtures_map = {}
+
+ fixture_sets = files_to_read.map do |fs_name|
+ klass = class_names[fs_name]
+ conn = klass ? klass.connection : connection
+ fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
+ conn,
+ fs_name,
+ klass,
+ ::File.join(fixtures_directory, fs_name))
+ end
- table_rows.each_key do |table|
- unless deleted_tables[conn].include? table
- conn.delete "DELETE FROM #{conn.quote_table_name(table)}", "Fixture Delete"
- end
- deleted_tables[conn] << table
- end
+ update_all_loaded_fixtures fixtures_map
+ fixture_sets_by_connection = fixture_sets.group_by { |fs| fs.model_class ? fs.model_class.connection : connection }
- table_rows.each do |fixture_set_name, rows|
- conn.insert_fixtures(rows, fixture_set_name)
- end
+ fixture_sets_by_connection.each do |conn, set|
+ table_rows_for_connection = Hash.new { |h, k| h[k] = [] }
- # Cap primary key sequences to max(pk).
- if conn.respond_to?(:reset_pk_sequence!)
- conn.reset_pk_sequence!(fs.table_name)
- end
+ set.each do |fs|
+ fs.table_rows.each do |table, rows|
+ table_rows_for_connection[table].unshift(*rows)
end
end
+ conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys)
- cache_fixtures(connection, fixtures_map)
+ # Cap primary key sequences to max(pk).
+ if conn.respond_to?(:reset_pk_sequence!)
+ set.each { |fs| conn.reset_pk_sequence!(fs.table_name) }
+ end
end
+
+ cache_fixtures(connection, fixtures_map)
end
cached_fixtures(connection, fixture_set_names)
end
@@ -883,6 +874,7 @@ module ActiveRecord
class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances
class_attribute :pre_loaded_fixtures, default: false
class_attribute :config, default: ActiveRecord::Base
+ class_attribute :lock_threads, default: true
end
module ClassMethods
@@ -982,7 +974,7 @@ module ActiveRecord
@fixture_connections = enlist_fixture_connections
@fixture_connections.each do |connection|
connection.begin_transaction joinable: false
- connection.pool.lock_thread = true
+ connection.pool.lock_thread = true if lock_threads
end
# When connections are established in the future, begin a transaction too
@@ -998,7 +990,7 @@ module ActiveRecord
if connection && !@fixture_connections.include?(connection)
connection.begin_transaction joinable: false
- connection.pool.lock_thread = true
+ connection.pool.lock_thread = true if lock_threads
@fixture_connections << connection
end
end
diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb
index 7ccb57b305..72035a986b 100644
--- a/activerecord/lib/active_record/gem_version.rb
+++ b/activerecord/lib/active_record/gem_version.rb
@@ -7,8 +7,8 @@ module ActiveRecord
end
module VERSION
- MAJOR = 5
- MINOR = 2
+ MAJOR = 6
+ MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 0715c11473..6891c575c7 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -47,22 +47,21 @@ module ActiveRecord
# Determines if one of the attributes passed in is the inheritance column,
# and if the inheritance column is attr accessible, it initializes an
# instance of the given subclass instead of the base class.
- def new(*args, &block)
+ def new(attributes = nil, &block)
if abstract_class? || self == Base
raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
end
- attrs = args.first
if has_attribute?(inheritance_column)
- subclass = subclass_from_attributes(attrs)
+ subclass = subclass_from_attributes(attributes)
- if subclass.nil? && base_class == self
+ if subclass.nil? && base_class?
subclass = subclass_from_attributes(column_defaults)
end
end
if subclass && subclass != self
- subclass.new(*args, &block)
+ subclass.new(attributes, &block)
else
super
end
@@ -105,21 +104,53 @@ module ActiveRecord
end
end
- # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
- # If you are using inheritance with ActiveRecord and don't want child classes
- # to utilize the implied STI table name of the parent class, this will need to be true.
- # For example, given the following:
+ # Returns whether the class is a base class.
+ # See #base_class for more information.
+ def base_class?
+ base_class == self
+ end
+
+ # Set this to +true+ if this is an abstract class (see
+ # <tt>abstract_class?</tt>).
+ # If you are using inheritance with Active Record and don't want a class
+ # to be considered as part of the STI hierarchy, you must set this to
+ # true.
+ # +ApplicationRecord+, for example, is generated as an abstract class.
+ #
+ # Consider the following default behaviour:
+ #
+ # Shape = Class.new(ActiveRecord::Base)
+ # Polygon = Class.new(Shape)
+ # Square = Class.new(Polygon)
+ #
+ # Shape.table_name # => "shapes"
+ # Polygon.table_name # => "shapes"
+ # Square.table_name # => "shapes"
+ # Shape.create! # => #<Shape id: 1, type: nil>
+ # Polygon.create! # => #<Polygon id: 2, type: "Polygon">
+ # Square.create! # => #<Square id: 3, type: "Square">
#
- # class SuperClass < ActiveRecord::Base
+ # However, when using <tt>abstract_class</tt>, +Shape+ is omitted from
+ # the hierarchy:
+ #
+ # class Shape < ActiveRecord::Base
# self.abstract_class = true
# end
- # class Child < SuperClass
- # self.table_name = 'the_table_i_really_want'
- # end
- #
+ # Polygon = Class.new(Shape)
+ # Square = Class.new(Polygon)
#
- # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt>
+ # Shape.table_name # => nil
+ # Polygon.table_name # => "polygons"
+ # Square.table_name # => "polygons"
+ # Shape.create! # => NotImplementedError: Shape is an abstract class and cannot be instantiated.
+ # Polygon.create! # => #<Polygon id: 1, type: nil>
+ # Square.create! # => #<Square id: 2, type: "Square">
#
+ # Note that in the above example, to disallow the creation of a plain
+ # +Polygon+, you should use <tt>validates :type, presence: true</tt>,
+ # instead of setting it as an abstract class. This way, +Polygon+ will
+ # stay in the hierarchy, and Active Record will continue to correctly
+ # derive the table name.
attr_accessor :abstract_class
# Returns whether this class is an abstract class or not.
@@ -131,6 +162,10 @@ module ActiveRecord
store_full_sti_class ? name : name.demodulize
end
+ def polymorphic_name
+ base_class.name
+ end
+
def inherited(subclass)
subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new)
super
diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb
index 14795cc815..3626a13d7c 100644
--- a/activerecord/lib/active_record/internal_metadata.rb
+++ b/activerecord/lib/active_record/internal_metadata.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "scoping/default"
-require_relative "scoping/named"
+require "active_record/scoping/default"
+require "active_record/scoping/named"
module ActiveRecord
# This class is used to create a table that keeps track of values and keys such
@@ -17,7 +17,7 @@ module ActiveRecord
end
def []=(key, value)
- find_or_initialize_by(key: key).update_attributes!(value: value)
+ find_or_initialize_by(key: key).update!(value: value)
end
def [](key)
diff --git a/activerecord/lib/active_record/legacy_yaml_adapter.rb b/activerecord/lib/active_record/legacy_yaml_adapter.rb
index 23644aab8f..ffa095dd94 100644
--- a/activerecord/lib/active_record/legacy_yaml_adapter.rb
+++ b/activerecord/lib/active_record/legacy_yaml_adapter.rb
@@ -8,7 +8,7 @@ module ActiveRecord
case coder["active_record_yaml_version"]
when 1, 2 then coder
else
- if coder["attributes"].is_a?(AttributeSet)
+ if coder["attributes"].is_a?(ActiveModel::AttributeSet)
Rails420.convert(klass, coder)
else
Rails41.convert(klass, coder)
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index e1e24e2814..7f096bb532 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -61,13 +61,6 @@ module ActiveRecord
end
private
-
- def increment_lock
- lock_col = self.class.locking_column
- previous_lock_value = send(lock_col)
- send("#{lock_col}=", previous_lock_value + 1)
- end
-
def _create_record(attribute_names = self.attribute_names, *)
if locking_enabled?
# We always want to persist the locking version, even if we don't detect
@@ -77,63 +70,56 @@ module ActiveRecord
super
end
- def _update_record(attribute_names = self.attribute_names)
+ def _touch_row(attribute_names, time)
+ super
+ ensure
+ clear_attribute_change(self.class.locking_column) if locking_enabled?
+ end
+
+ def _update_row(attribute_names, attempted_action = "update")
return super unless locking_enabled?
- return 0 if attribute_names.empty?
begin
- lock_col = self.class.locking_column
-
- previous_lock_value = read_attribute_before_type_cast(lock_col)
-
- increment_lock
-
- attribute_names.push(lock_col)
+ locking_column = self.class.locking_column
+ previous_lock_value = read_attribute_before_type_cast(locking_column)
+ attribute_names << locking_column
- relation = self.class.unscoped
+ self[locking_column] += 1
- affected_rows = relation.where(
- self.class.primary_key => id,
- lock_col => previous_lock_value
- ).update_all(
- attributes_for_update(attribute_names).map do |name|
- [name, _read_attribute(name)]
- end.to_h
+ affected_rows = self.class._update_record(
+ attributes_with_values(attribute_names),
+ self.class.primary_key => id_in_database,
+ locking_column => previous_lock_value
)
- unless affected_rows == 1
- raise ActiveRecord::StaleObjectError.new(self, "update")
+ if affected_rows != 1
+ raise ActiveRecord::StaleObjectError.new(self, attempted_action)
end
affected_rows
# If something went wrong, revert the locking_column value.
rescue Exception
- send("#{lock_col}=", previous_lock_value.to_i)
-
+ self[locking_column] = previous_lock_value.to_i
raise
end
end
def destroy_row
- affected_rows = super
-
- if locking_enabled? && affected_rows != 1
- raise ActiveRecord::StaleObjectError.new(self, "destroy")
- end
+ return super unless locking_enabled?
- affected_rows
- end
+ locking_column = self.class.locking_column
- def relation_for_destroy
- relation = super
+ affected_rows = self.class._delete_record(
+ self.class.primary_key => id_in_database,
+ locking_column => read_attribute_before_type_cast(locking_column)
+ )
- if locking_enabled?
- locking_column = self.class.locking_column
- relation = relation.where(locking_column => read_attribute_before_type_cast(locking_column))
+ if affected_rows != 1
+ raise ActiveRecord::StaleObjectError.new(self, "destroy")
end
- relation
+ affected_rows
end
module ClassMethods
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index 72bccd4906..5d1d15c94d 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -62,13 +62,14 @@ module ActiveRecord
# the locked record.
def lock!(lock = true)
if persisted?
- if changed?
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Locking a record with unpersisted changes is deprecated and will raise an
- exception in Rails 5.2. Use `save` to persist the changes, or `reload` to
- discard them explicitly.
+ if has_changes_to_save?
+ raise(<<-MSG.squish)
+ Locking a record with unpersisted changes is not supported. Use
+ `save` to persist the changes, or `reload` to discard them
+ explicitly.
MSG
end
+
reload(lock: lock)
end
self
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 405f3a30c6..013c3765b2 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -90,6 +90,47 @@ module ActiveRecord
def logger
ActiveRecord::Base.logger
end
+
+ def debug(progname = nil, &block)
+ return unless super
+
+ if ActiveRecord::Base.verbose_query_logs
+ log_query_source
+ end
+ end
+
+ def log_query_source
+ source_line, line_number = extract_callstack(caller_locations)
+
+ if source_line
+ if defined?(::Rails.root)
+ app_root = "#{::Rails.root.to_s}/".freeze
+ source_line = source_line.sub(app_root, "")
+ end
+
+ logger.debug(" ↳ #{ source_line }:#{ line_number }")
+ end
+ end
+
+ def extract_callstack(callstack)
+ line = callstack.find do |frame|
+ frame.absolute_path && !ignored_callstack(frame.absolute_path)
+ end
+
+ offending_line = line || callstack.first
+
+ [
+ offending_line.path,
+ offending_line.lineno
+ ]
+ end
+
+ RAILS_GEM_ROOT = File.expand_path("../../..", __dir__) + "/"
+
+ def ignored_callstack(path)
+ path.start_with?(RAILS_GEM_ROOT) ||
+ path.start_with?(RbConfig::CONFIG["rubylibdir"])
+ end
end
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 8845e26ab7..025201c20b 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require "benchmark"
require "set"
require "zlib"
require "active_support/core_ext/module/attribute_accessors"
@@ -140,6 +141,7 @@ module ActiveRecord
class ConcurrentMigrationError < MigrationError #:nodoc:
DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze
+ RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock".freeze
def initialize(message = DEFAULT_MESSAGE)
super
@@ -550,7 +552,7 @@ module ActiveRecord
end
def call(env)
- mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
+ mtime = ActiveRecord::Base.connection.migration_context.last_migration.mtime.to_i
if @last_check < mtime
ActiveRecord::Migration.check_pending!(connection)
@last_check = mtime
@@ -575,13 +577,14 @@ module ActiveRecord
# Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending.
def check_pending!(connection = Base.connection)
- raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection)
+ raise ActiveRecord::PendingMigrationError if connection.migration_context.needs_migration?
end
def load_schema_if_pending!
- if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations?
+ if Base.connection.migration_context.needs_migration? || !Base.connection.migration_context.any_migrations?
# Roundtrip to Rake to allow plugins to hook into database initialization.
- FileUtils.cd Rails.root do
+ root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root
+ FileUtils.cd(root) do
current_config = Base.connection_config
Base.clear_all_connections!
system("bin/rails db:test:prepare")
@@ -733,6 +736,24 @@ module ActiveRecord
execute_block { yield helper }
end
+ # Used to specify an operation that is only run when migrating up
+ # (for example, populating a new column with its initial values).
+ #
+ # In the following example, the new column +published+ will be given
+ # the value +true+ for all existing records.
+ #
+ # class AddPublishedToPosts < ActiveRecord::Migration[5.2]
+ # def change
+ # add_column :posts, :published, :boolean, default: false
+ # up_only do
+ # execute "update posts set published = 'true'"
+ # end
+ # end
+ # end
+ def up_only
+ execute_block { yield } unless reverting?
+ end
+
# Runs the given migration classes.
# Last argument can specify options:
# - :direction (default is :up)
@@ -857,10 +878,10 @@ module ActiveRecord
FileUtils.mkdir_p(destination) unless File.exist?(destination)
- destination_migrations = ActiveRecord::Migrator.migrations(destination)
+ destination_migrations = ActiveRecord::MigrationContext.new(destination).migrations
last = destination_migrations.last
sources.each do |scope, path|
- source_migrations = ActiveRecord::Migrator.migrations(path)
+ source_migrations = ActiveRecord::MigrationContext.new(path).migrations
source_migrations.each do |migration|
source = File.binread(migration.filename)
@@ -978,136 +999,147 @@ module ActiveRecord
end
end
- class Migrator#:nodoc:
- class << self
- attr_writer :migrations_paths
- alias :migrations_path= :migrations_paths=
-
- def migrate(migrations_paths, target_version = nil, &block)
- case
- when target_version.nil?
- up(migrations_paths, target_version, &block)
- when current_version == 0 && target_version == 0
- []
- when current_version > target_version
- down(migrations_paths, target_version, &block)
- else
- up(migrations_paths, target_version, &block)
- end
- end
+ class MigrationContext # :nodoc:
+ attr_reader :migrations_paths
- def rollback(migrations_paths, steps = 1)
- move(:down, migrations_paths, steps)
- end
+ def initialize(migrations_paths)
+ @migrations_paths = migrations_paths
+ end
- def forward(migrations_paths, steps = 1)
- move(:up, migrations_paths, steps)
+ def migrate(target_version = nil, &block)
+ case
+ when target_version.nil?
+ up(target_version, &block)
+ when current_version == 0 && target_version == 0
+ []
+ when current_version > target_version
+ down(target_version, &block)
+ else
+ up(target_version, &block)
end
+ end
+
+ def rollback(steps = 1)
+ move(:down, steps)
+ end
- def up(migrations_paths, target_version = nil)
- migrations = migrations(migrations_paths)
- migrations.select! { |m| yield m } if block_given?
+ def forward(steps = 1)
+ move(:up, steps)
+ end
- new(:up, migrations, target_version).migrate
+ def up(target_version = nil)
+ selected_migrations = if block_given?
+ migrations.select { |m| yield m }
+ else
+ migrations
end
- def down(migrations_paths, target_version = nil)
- migrations = migrations(migrations_paths)
- migrations.select! { |m| yield m } if block_given?
+ Migrator.new(:up, selected_migrations, target_version).migrate
+ end
- new(:down, migrations, target_version).migrate
+ def down(target_version = nil)
+ selected_migrations = if block_given?
+ migrations.select { |m| yield m }
+ else
+ migrations
end
- def run(direction, migrations_paths, target_version)
- new(direction, migrations(migrations_paths), target_version).run
- end
+ Migrator.new(:down, selected_migrations, target_version).migrate
+ end
- def open(migrations_paths)
- new(:up, migrations(migrations_paths), nil)
- end
+ def run(direction, target_version)
+ Migrator.new(direction, migrations, target_version).run
+ end
- def schema_migrations_table_name
- SchemaMigration.table_name
- end
- deprecate :schema_migrations_table_name
+ def open
+ Migrator.new(:up, migrations, nil)
+ end
- def get_all_versions(connection = Base.connection)
- if SchemaMigration.table_exists?
- SchemaMigration.all_versions.map(&:to_i)
- else
- []
- end
+ def get_all_versions
+ if SchemaMigration.table_exists?
+ SchemaMigration.all_versions.map(&:to_i)
+ else
+ []
end
+ end
- def current_version(connection = Base.connection)
- get_all_versions(connection).max || 0
- end
+ def current_version
+ get_all_versions.max || 0
+ rescue ActiveRecord::NoDatabaseError
+ end
- def needs_migration?(connection = Base.connection)
- (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0
- end
+ def needs_migration?
+ (migrations.collect(&:version) - get_all_versions).size > 0
+ end
- def any_migrations?
- migrations(migrations_paths).any?
- end
+ def any_migrations?
+ migrations.any?
+ end
- def last_migration #:nodoc:
- migrations(migrations_paths).last || NullMigration.new
- end
+ def last_migration #:nodoc:
+ migrations.last || NullMigration.new
+ end
- def migrations_paths
- @migrations_paths ||= ["db/migrate"]
- # just to not break things if someone uses: migrations_path = some_string
- Array(@migrations_paths)
- end
+ def parse_migration_filename(filename) # :nodoc:
+ File.basename(filename).scan(Migration::MigrationFilenameRegexp).first
+ end
+
+ def migrations
+ migrations = migration_files.map do |file|
+ version, name, scope = parse_migration_filename(file)
+ raise IllegalMigrationNameError.new(file) unless version
+ version = version.to_i
+ name = name.camelize
- def parse_migration_filename(filename) # :nodoc:
- File.basename(filename).scan(Migration::MigrationFilenameRegexp).first
+ MigrationProxy.new(name, version, file, scope)
end
- def migrations(paths)
- paths = Array(paths)
+ migrations.sort_by(&:version)
+ end
- migrations = migration_files(paths).map do |file|
- version, name, scope = parse_migration_filename(file)
- raise IllegalMigrationNameError.new(file) unless version
- version = version.to_i
- name = name.camelize
+ def migrations_status
+ db_list = ActiveRecord::SchemaMigration.normalized_versions
- MigrationProxy.new(name, version, file, scope)
- end
+ file_list = migration_files.map do |file|
+ version, name, scope = parse_migration_filename(file)
+ raise IllegalMigrationNameError.new(file) unless version
+ version = ActiveRecord::SchemaMigration.normalize_migration_number(version)
+ status = db_list.delete(version) ? "up" : "down"
+ [status, version, (name + scope).humanize]
+ end.compact
- migrations.sort_by(&:version)
+ db_list.map! do |version|
+ ["up", version, "********** NO FILE **********"]
end
- def migrations_status(paths)
- paths = Array(paths)
-
- db_list = ActiveRecord::SchemaMigration.normalized_versions
+ (db_list + file_list).sort_by { |_, version, _| version }
+ end
- file_list = migration_files(paths).map do |file|
- version, name, scope = parse_migration_filename(file)
- raise IllegalMigrationNameError.new(file) unless version
- version = ActiveRecord::SchemaMigration.normalize_migration_number(version)
- status = db_list.delete(version) ? "up" : "down"
- [status, version, (name + scope).humanize]
- end.compact
+ def migration_files
+ paths = Array(migrations_paths)
+ Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
+ end
- db_list.map! do |version|
- ["up", version, "********** NO FILE **********"]
- end
+ def current_environment
+ ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+ end
- (db_list + file_list).sort_by { |_, version, _| version }
- end
+ def protected_environment?
+ ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment
+ end
- def migration_files(paths)
- Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
- end
+ def last_stored_environment
+ return nil if current_version == 0
+ raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists?
- private
+ environment = ActiveRecord::InternalMetadata[:environment]
+ raise NoEnvironmentInSchemaError unless environment
+ environment
+ end
- def move(direction, migrations_paths, steps)
- migrator = new(direction, migrations(migrations_paths))
+ private
+ def move(direction, steps)
+ migrator = Migrator.new(direction, migrations)
if current_version != 0 && !migrator.current_migration
raise UnknownMigrationVersionError.new(current_version)
@@ -1122,10 +1154,29 @@ module ActiveRecord
finish = migrator.migrations[start_index + steps]
version = finish ? finish.version : 0
- send(direction, migrations_paths, version)
+ send(direction, version)
+ end
+ end
+
+ class Migrator # :nodoc:
+ class << self
+ attr_accessor :migrations_paths
+
+ def migrations_path=(path)
+ ActiveSupport::Deprecation.warn \
+ "ActiveRecord::Migrator.migrations_paths= is now deprecated and will be removed in Rails 6.0." \
+ "You can set the `migrations_paths` on the `connection` instead through the `database.yml`."
+ self.migrations_paths = [path]
+ end
+
+ # For cases where a table doesn't exist like loading from schema cache
+ def current_version
+ MigrationContext.new(migrations_paths).current_version
end
end
+ self.migrations_paths = ["db/migrate"]
+
def initialize(direction, migrations, target_version = nil)
@direction = direction
@target_version = target_version
@@ -1188,7 +1239,7 @@ module ActiveRecord
end
def load_migrated
- @migrated_versions = Set.new(self.class.get_all_versions)
+ @migrated_versions = Set.new(Base.connection.migration_context.get_all_versions)
end
private
@@ -1220,7 +1271,7 @@ module ActiveRecord
# Stores the current environment in the database.
def record_environment
return if down?
- ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
+ ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment
end
def ran?(migration)
@@ -1229,7 +1280,7 @@ module ActiveRecord
# Return true if a valid version is not provided.
def invalid_target?
- !target && @target_version && @target_version > 0
+ @target_version && @target_version != 0 && !target
end
def execute_migration_in_transaction(migration, direction)
@@ -1262,10 +1313,10 @@ module ActiveRecord
end
def validate(migrations)
- name , = migrations.group_by(&:name).find { |_, v| v.length > 1 }
+ name, = migrations.group_by(&:name).find { |_, v| v.length > 1 }
raise DuplicateMigrationNameError.new(name) if name
- version , = migrations.group_by(&:version).find { |_, v| v.length > 1 }
+ version, = migrations.group_by(&:version).find { |_, v| v.length > 1 }
raise DuplicateMigrationVersionError.new(version) if version
end
@@ -1279,23 +1330,6 @@ module ActiveRecord
end
end
- def self.last_stored_environment
- return nil if current_version == 0
- raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists?
-
- environment = ActiveRecord::InternalMetadata[:environment]
- raise NoEnvironmentInSchemaError unless environment
- environment
- end
-
- def self.current_environment
- ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
- end
-
- def self.protected_environment?
- ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment
- end
-
def up?
@direction == :up
end
@@ -1323,12 +1357,17 @@ module ActiveRecord
def with_advisory_lock
lock_id = generate_migrator_advisory_lock_id
- got_lock = Base.connection.get_advisory_lock(lock_id)
+ connection = Base.connection
+ got_lock = connection.get_advisory_lock(lock_id)
raise ConcurrentMigrationError unless got_lock
load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
yield
ensure
- Base.connection.release_advisory_lock(lock_id) if got_lock
+ if got_lock && !connection.release_advisory_lock(lock_id)
+ raise ConcurrentMigrationError.new(
+ ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE
+ )
+ end
end
MIGRATOR_SALT = 2053462845
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index a3a5e0fa16..087632b10f 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -85,7 +85,7 @@ module ActiveRecord
# invert the +command+.
def inverse_of(command, args, &block)
method = :"invert_#{command}"
- raise IrreversibleMigration, <<-MSG.strip_heredoc unless respond_to?(method, true)
+ raise IrreversibleMigration, <<~MSG unless respond_to?(method, true)
This migration uses #{command}, which is not automatically reversible.
To make the migration reversible you can either:
1. Define #up and #down methods in place of the #change method.
@@ -110,7 +110,7 @@ module ActiveRecord
private
- module StraightReversions
+ module StraightReversions # :nodoc:
private
{ transaction: :transaction,
execute_block: :execute_block,
@@ -161,8 +161,8 @@ module ActiveRecord
table, columns, options = *args
options ||= {}
- index_name = options[:name]
- options_hash = index_name ? { name: index_name } : { column: columns }
+ options_hash = options.slice(:name, :algorithm)
+ options_hash[:column] = columns if !options_hash[:name]
[:remove_index, [table, options_hash]]
end
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
index 784292f3f9..0edaaa0cf9 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -13,13 +13,41 @@ module ActiveRecord
const_get(name)
end
- V5_2 = Current
+ V6_0 = Current
+
+ class V5_2 < V6_0
+ end
class V5_1 < V5_2
+ def change_column(table_name, column_name, type, options = {})
+ if adapter_name == "PostgreSQL"
+ clear_cache!
+ sql = connection.send(:change_column_sql, table_name, column_name, type, options)
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql}"
+ change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
+ change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
+ else
+ super
+ end
+ end
+
+ def create_table(table_name, options = {})
+ if adapter_name == "Mysql2"
+ super(table_name, options: "ENGINE=InnoDB", **options)
+ else
+ super
+ end
+ end
end
class V5_0 < V5_1
module TableDefinition
+ def primary_key(name, type = :primary_key, **options)
+ type = :integer if type == :primary_key
+ super
+ end
+
def references(*args, **options)
super(*args, type: :integer, **options)
end
@@ -47,11 +75,8 @@ module ActiveRecord
end
if block_given?
- super(table_name, options) do |t|
- class << t
- prepend TableDefinition
- end
- yield t
+ super do |t|
+ yield compatible_table_definition(t)
end
else
super
@@ -60,21 +85,46 @@ module ActiveRecord
def change_table(table_name, options = {})
if block_given?
- super(table_name, options) do |t|
- class << t
- prepend TableDefinition
- end
- yield t
+ super do |t|
+ yield compatible_table_definition(t)
end
else
super
end
end
+ def create_join_table(table_1, table_2, column_options: {}, **options)
+ column_options.reverse_merge!(type: :integer)
+
+ if block_given?
+ super do |t|
+ yield compatible_table_definition(t)
+ end
+ else
+ super
+ end
+ end
+
+ def add_column(table_name, column_name, type, options = {})
+ if type == :primary_key
+ type = :integer
+ options[:primary_key] = true
+ end
+ super
+ end
+
def add_reference(table_name, ref_name, **options)
super(table_name, ref_name, type: :integer, **options)
end
alias :add_belongs_to :add_reference
+
+ private
+ def compatible_table_definition(t)
+ class << t
+ prepend TableDefinition
+ end
+ t
+ end
end
class V4_2 < V5_0
@@ -93,11 +143,8 @@ module ActiveRecord
def create_table(table_name, options = {})
if block_given?
- super(table_name, options) do |t|
- class << t
- prepend TableDefinition
- end
- yield t
+ super do |t|
+ yield compatible_table_definition(t)
end
else
super
@@ -106,11 +153,8 @@ module ActiveRecord
def change_table(table_name, options = {})
if block_given?
- super(table_name, options) do |t|
- class << t
- prepend TableDefinition
- end
- yield t
+ super do |t|
+ yield compatible_table_definition(t)
end
else
super
@@ -146,6 +190,12 @@ module ActiveRecord
end
private
+ def compatible_table_definition(t)
+ class << t
+ prepend TableDefinition
+ end
+ super
+ end
def index_name_for_remove(table_name, options = {})
index_name = index_name(table_name, options)
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 34c0ef4e75..694ff85fa1 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -87,19 +87,6 @@ module ActiveRecord
# Sets the name of the internal metadata table.
##
- # :singleton-method: protected_environments
- # :call-seq: protected_environments
- #
- # The array of names of environments where destructive actions should be prohibited. By default,
- # the value is <tt>["production"]</tt>.
-
- ##
- # :singleton-method: protected_environments=
- # :call-seq: protected_environments=(environments)
- #
- # Sets an array of names of environments where destructive actions should be prohibited.
-
- ##
# :singleton-method: pluralize_table_names
# :call-seq: pluralize_table_names
#
@@ -115,20 +102,6 @@ module ActiveRecord
# If true, the default table name for a Product class will be "products". If false, it would just be "product".
# See table_name for the full rules on table/class naming. This is true, by default.
- ##
- # :singleton-method: ignored_columns
- # :call-seq: ignored_columns
- #
- # The list of columns names the model should ignore. Ignored columns won't have attribute
- # accessors defined, and won't be referenced in SQL queries.
-
- ##
- # :singleton-method: ignored_columns=
- # :call-seq: ignored_columns=(columns)
- #
- # Sets the columns names the model should ignore. Ignored columns won't have attribute
- # accessors defined, and won't be referenced in SQL queries.
-
included do
mattr_accessor :primary_key_prefix_type, instance_writer: false
@@ -136,11 +109,11 @@ module ActiveRecord
class_attribute :table_name_suffix, instance_writer: false, default: ""
class_attribute :schema_migrations_table_name, instance_accessor: false, default: "schema_migrations"
class_attribute :internal_metadata_table_name, instance_accessor: false, default: "ar_internal_metadata"
- class_attribute :protected_environments, instance_accessor: false, default: [ "production" ]
class_attribute :pluralize_table_names, instance_writer: false, default: true
- class_attribute :ignored_columns, instance_accessor: false, default: [].freeze
+ self.protected_environments = ["production"]
self.inheritance_column = "type"
+ self.ignored_columns = [].freeze
delegate :type_for_attribute, to: :class
@@ -252,6 +225,21 @@ module ActiveRecord
(parents.detect { |p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix
end
+ # The array of names of environments where destructive actions should be prohibited. By default,
+ # the value is <tt>["production"]</tt>.
+ def protected_environments
+ if defined?(@protected_environments)
+ @protected_environments
+ else
+ superclass.protected_environments
+ end
+ end
+
+ # Sets an array of names of environments where destructive actions should be prohibited.
+ def protected_environments=(environments)
+ @protected_environments = environments.map(&:to_s)
+ end
+
# Defines the name of the table column which will store the class name on single-table
# inheritance situations.
#
@@ -271,8 +259,24 @@ module ActiveRecord
@explicit_inheritance_column = true
end
+ # The list of columns names the model should ignore. Ignored columns won't have attribute
+ # accessors defined, and won't be referenced in SQL queries.
+ def ignored_columns
+ if defined?(@ignored_columns)
+ @ignored_columns
+ else
+ superclass.ignored_columns
+ end
+ end
+
+ # Sets the columns names the model should ignore. Ignored columns won't have attribute
+ # accessors defined, and won't be referenced in SQL queries.
+ def ignored_columns=(columns)
+ @ignored_columns = columns.map(&:to_s)
+ end
+
def sequence_name
- if base_class == self
+ if base_class?
@sequence_name ||= reset_sequence_name
else
(@sequence_name ||= nil) || base_class.sequence_name
@@ -321,11 +325,11 @@ module ActiveRecord
end
def attributes_builder # :nodoc:
- @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) do |name|
- unless columns_hash.key?(name)
- _default_attributes[name].dup
- end
+ unless defined?(@attributes_builder) && @attributes_builder
+ defaults = _default_attributes.except(*(column_names - [primary_key]))
+ @attributes_builder = ActiveModel::AttributeSet::Builder.new(attribute_types, defaults)
end
+ @attributes_builder
end
def columns_hash # :nodoc:
@@ -344,7 +348,7 @@ module ActiveRecord
end
def yaml_encoder # :nodoc:
- @yaml_encoder ||= AttributeSet::YAMLEncoder.new(attribute_types)
+ @yaml_encoder ||= ActiveModel::AttributeSet::YAMLEncoder.new(attribute_types)
end
# Returns the type of the attribute with the given name, after applying
@@ -357,8 +361,9 @@ module ActiveRecord
# it).
#
# +attr_name+ The name of the attribute to retrieve the type for. Must be
- # a string
+ # a string or a symbol.
def type_for_attribute(attr_name, &block)
+ attr_name = attr_name.to_s
if block
attribute_types.fetch(attr_name, &block)
else
@@ -374,7 +379,8 @@ module ActiveRecord
end
def _default_attributes # :nodoc:
- @default_attributes ||= AttributeSet.new({})
+ load_schema
+ @default_attributes ||= ActiveModel::AttributeSet.new({})
end
# Returns an array of column names as strings.
@@ -421,7 +427,7 @@ module ActiveRecord
# end
def reset_column_information
connection.clear_cache!
- undefine_attribute_methods
+ ([self] + descendants).each(&:undefine_attribute_methods)
connection.schema_cache.clear_data_source_cache!(table_name)
reload_schema_from_cache
@@ -495,8 +501,7 @@ module ActiveRecord
# Computes and returns a table name according to default conventions.
def compute_table_name
- base = base_class
- if self == base
+ if base_class?
# Nested classes are prefixed with singular parent table name.
if parent < Base && !parent.abstract_class?
contained = parent.table_name
@@ -507,7 +512,7 @@ module ActiveRecord
"#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
else
# STI subclasses always use their superclass' table.
- base.table_name
+ base_class.table_name
end
end
end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 435c81c153..fa20bce3a9 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -63,6 +63,18 @@ module ActiveRecord
# member.update params[:member]
# member.avatar.icon # => 'sad'
#
+ # If you want to update the current avatar without providing the id, you must add <tt>:update_only</tt> option.
+ #
+ # class Member < ActiveRecord::Base
+ # has_one :avatar
+ # accepts_nested_attributes_for :avatar, update_only: true
+ # end
+ #
+ # params = { member: { avatar_attributes: { icon: 'sad' } } }
+ # member.update params[:member]
+ # member.avatar.id # => 2
+ # member.avatar.icon # => 'sad'
+ #
# By default you will only be able to set and update attributes on the
# associated model. If you want to destroy the associated model through the
# attributes hash, you have to enable it first using the
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index b48a137a73..c2393c1fc8 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -99,7 +99,9 @@ module ActiveRecord
# for updating all records in a single query.
def update(id = :all, attributes)
if id.is_a?(Array)
- id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }.compact
+ id.map { |one_id| find(one_id) }.each_with_index { |object, idx|
+ object.update(attributes[idx])
+ }
elsif id == :all
all.each { |record| record.update(attributes) }
else
@@ -112,7 +114,6 @@ module ActiveRecord
object.update(attributes)
object
end
- rescue RecordNotFound
end
# Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
@@ -136,11 +137,10 @@ module ActiveRecord
# Todo.destroy(todos)
def destroy(id)
if id.is_a?(Array)
- id.map { |one_id| destroy(one_id) }.compact
+ find(id).each(&:destroy)
else
find(id).destroy
end
- rescue RecordNotFound
end
# Deletes the row with a primary key matching the +id+ argument, using a
@@ -165,6 +165,48 @@ module ActiveRecord
where(primary_key => id_or_array).delete_all
end
+ def _insert_record(values) # :nodoc:
+ primary_key_value = nil
+
+ if primary_key && Hash === values
+ primary_key_value = values[primary_key]
+
+ if !primary_key_value && prefetch_primary_key?
+ primary_key_value = next_sequence_value
+ values[primary_key] = primary_key_value
+ end
+ end
+
+ if values.empty?
+ im = arel_table.compile_insert(connection.empty_insert_statement_value(primary_key))
+ im.into arel_table
+ else
+ im = arel_table.compile_insert(_substitute_values(values))
+ end
+
+ connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
+ end
+
+ def _update_record(values, constraints) # :nodoc:
+ constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) }
+
+ um = arel_table.where(
+ constraints.reduce(&:and)
+ ).compile_update(_substitute_values(values), primary_key)
+
+ connection.update(um, "#{self} Update")
+ end
+
+ def _delete_record(constraints) # :nodoc:
+ constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) }
+
+ dm = Arel::DeleteManager.new
+ dm.from(arel_table)
+ dm.wheres = constraints
+
+ connection.delete(dm, "#{self} Destroy")
+ end
+
private
# Called by +instantiate+ to decide which class to use for a new
# record instance.
@@ -174,6 +216,14 @@ module ActiveRecord
def discriminate_class_for_record(record)
self
end
+
+ def _substitute_values(values)
+ values.map do |name, value|
+ attr = arel_attribute(name)
+ bind = predicate_builder.build_bind_attribute(name, value)
+ [attr, bind]
+ end
+ end
end
# Returns true if this object hasn't been saved yet -- that is, a record
@@ -271,7 +321,7 @@ module ActiveRecord
# callbacks or any <tt>:dependent</tt> association
# options, use <tt>#destroy</tt>.
def delete
- _relation_for_itself.delete_all if persisted?
+ _delete_row if persisted?
@destroyed = true
freeze
end
@@ -320,9 +370,10 @@ module ActiveRecord
# Any change to the attributes on either instance will affect both instances.
# If you want to change the sti column as well, use #becomes! instead.
def becomes(klass)
- became = klass.new
+ became = klass.allocate
+ became.send(:initialize)
became.instance_variable_set("@attributes", @attributes)
- became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker)
+ became.instance_variable_set("@mutations_from_database", @mutations_from_database) if defined?(@mutations_from_database)
became.instance_variable_set("@changed_attributes", attributes_changed_by_setter)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
@@ -363,11 +414,7 @@ module ActiveRecord
verify_readonly_attribute(name)
public_send("#{name}=", value)
- if has_changes_to_save?
- save(validate: false)
- else
- true
- end
+ save(validate: false)
end
# Updates the attributes of the model from the passed-in hash and saves the
@@ -383,6 +430,7 @@ module ActiveRecord
end
alias update_attributes update
+ deprecate :update_attributes
# Updates its receiver just like #update but calls #save! instead
# of +save+, so an exception is raised if the record is invalid and saving will fail.
@@ -396,6 +444,7 @@ module ActiveRecord
end
alias update_attributes! update!
+ deprecate :update_attributes!
# Equivalent to <code>update_columns(name => value)</code>.
def update_column(name, value)
@@ -426,13 +475,16 @@ module ActiveRecord
verify_readonly_attribute(key.to_s)
end
- updated_count = _relation_for_itself.update_all(attributes)
+ affected_rows = self.class._update_record(
+ attributes,
+ self.class.primary_key => id_in_database
+ )
attributes.each do |k, v|
write_attribute_without_type_cast(k, v)
end
- updated_count == 1
+ affected_rows == 1
end
# Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1).
@@ -605,35 +657,12 @@ module ActiveRecord
MSG
end
- time ||= current_time_from_proper_timezone
- attributes = timestamp_attributes_for_update_in_model
- attributes.concat(names)
-
- unless attributes.empty?
- changes = {}
-
- attributes.each do |column|
- column = column.to_s
- changes[column] = write_attribute(column, time)
- end
-
- scope = _relation_for_itself
-
- if locking_enabled?
- locking_column = self.class.locking_column
- scope = scope.where(locking_column => read_attribute_before_type_cast(locking_column))
- changes[locking_column] = increment_lock
- end
-
- clear_attribute_changes(changes.keys)
- result = scope.update_all(changes) == 1
-
- if !result && locking_enabled?
- raise ActiveRecord::StaleObjectError.new(self, "touch")
- end
+ attribute_names = timestamp_attributes_for_update_in_model
+ attribute_names |= names.map(&:to_s)
- @_trigger_update_callback = result
- result
+ unless attribute_names.empty?
+ affected_rows = _touch_row(attribute_names, time)
+ @_trigger_update_callback = affected_rows == 1
else
true
end
@@ -646,19 +675,34 @@ module ActiveRecord
end
def destroy_row
- relation_for_destroy.delete_all
+ _delete_row
+ end
+
+ def _delete_row
+ self.class._delete_record(self.class.primary_key => id_in_database)
end
- def relation_for_destroy
- _relation_for_itself
+ def _touch_row(attribute_names, time)
+ time ||= current_time_from_proper_timezone
+
+ attribute_names.each do |attr_name|
+ write_attribute(attr_name, time)
+ clear_attribute_change(attr_name)
+ end
+
+ _update_row(attribute_names, "touch")
end
- def _relation_for_itself
- self.class.unscoped.where(self.class.primary_key => id)
+ def _update_row(attribute_names, attempted_action = "update")
+ self.class._update_record(
+ attributes_with_values(attribute_names),
+ self.class.primary_key => id_in_database
+ )
end
def create_or_update(*args, &block)
_raise_readonly_record_error if readonly?
+ return false if destroyed?
result = new_record? ? _create_record(&block) : _update_record(*args, &block)
result != false
end
@@ -666,26 +710,29 @@ module ActiveRecord
# Updates the associated record with values matching those of the instance attributes.
# Returns the number of affected rows.
def _update_record(attribute_names = self.attribute_names)
- attributes_values = arel_attributes_with_values_for_update(attribute_names)
- if attributes_values.empty?
- rows_affected = 0
+ attribute_names &= self.class.column_names
+ attribute_names = attributes_for_update(attribute_names)
+
+ if attribute_names.empty?
+ affected_rows = 0
@_trigger_update_callback = true
else
- rows_affected = self.class.unscoped._update_record attributes_values, id, id_in_database
- @_trigger_update_callback = rows_affected > 0
+ affected_rows = _update_row(attribute_names)
+ @_trigger_update_callback = affected_rows == 1
end
yield(self) if block_given?
- rows_affected
+ affected_rows
end
# Creates a record with values matching those of the instance attributes
# and returns its id.
def _create_record(attribute_names = self.attribute_names)
- attributes_values = arel_attributes_with_values_for_create(attribute_names)
+ attribute_names &= self.class.column_names
+ attributes_values = attributes_with_values_for_create(attribute_names)
- new_id = self.class.unscoped.insert attributes_values
+ new_id = self.class._insert_record(attributes_values)
self.id ||= new_id if self.class.primary_key
@new_record = false
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index 3d5babb8b7..28194c7c46 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -7,35 +7,31 @@ module ActiveRecord
# Enable the query cache within the block if Active Record is configured.
# If it's not, it will execute the given block.
def cache(&block)
- if configurations.empty?
- yield
- else
+ if connected? || !configurations.empty?
connection.cache(&block)
+ else
+ yield
end
end
# Disable the query cache within the block if Active Record is configured.
# If it's not, it will execute the given block.
def uncached(&block)
- if configurations.empty?
- yield
- else
+ if connected? || !configurations.empty?
connection.uncached(&block)
+ else
+ yield
end
end
end
def self.run
- caching_pool = ActiveRecord::Base.connection_pool
- caching_was_enabled = caching_pool.query_cache_enabled
-
- caching_pool.enable_query_cache!
-
- [caching_pool, caching_was_enabled]
+ ActiveRecord::Base.connection_handler.connection_pool_list.
+ reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! }
end
- def self.complete((caching_pool, caching_was_enabled))
- caching_pool.disable_query_cache! unless caching_was_enabled
+ def self.complete(pools)
+ pools.each { |pool| pool.disable_query_cache! }
ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
pool.release_connection if pool.active_connection? && !pool.connection.transaction_open?
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 3996d5661f..d33d36ac02 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -5,7 +5,7 @@ module ActiveRecord
delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all
delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all
delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
- delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
+ delegate :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, to: :all
delegate :find_by, :find_by!, to: :all
delegate :destroy_all, :delete_all, :update_all, to: :all
delegate :find_each, :find_in_batches, :in_batches, to: :all
@@ -13,7 +13,7 @@ module ActiveRecord
:where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending,
:having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all
delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
- delegate :pluck, :ids, to: :all
+ delegate :pluck, :pick, :ids, to: :all
# Executes a custom SQL query against your database and returns all the results. The results will
# be returned as an array with columns requested encapsulated as attributes of the model you call
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index ead42d64ec..6ab80a654d 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -53,16 +53,17 @@ module ActiveRecord
# to avoid cross references when loading a constant for the
# first time. Also, make it output to STDERR.
console do |app|
- require_relative "railties/console_sandbox" if app.sandbox?
- require_relative "base"
+ require "active_record/railties/console_sandbox" if app.sandbox?
+ require "active_record/base"
unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT)
console = ActiveSupport::Logger.new(STDERR)
Rails.logger.extend ActiveSupport::Logger.broadcast console
end
+ ActiveRecord::Base.verbose_query_logs = false
end
runner do
- require_relative "base"
+ require "active_record/base"
end
initializer "active_record.initialize_timezone" do
@@ -90,12 +91,16 @@ module ActiveRecord
filename = File.join(app.config.paths["db"].first, "schema_cache.yml")
if File.file?(filename)
+ current_version = ActiveRecord::Migrator.current_version
+
+ next if current_version.nil?
+
cache = YAML.load(File.read(filename))
- if cache.version == ActiveRecord::Migrator.current_version
+ if cache.version == current_version
connection.schema_cache = cache
connection_pool.schema_cache = cache.dup
else
- warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}."
+ warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{current_version}, but the one in the cache is #{cache.version}."
end
end
end
@@ -106,7 +111,7 @@ module ActiveRecord
initializer "active_record.warn_on_records_fetched_greater_than" do
if config.active_record.warn_on_records_fetched_greater_than
ActiveSupport.on_load(:active_record) do
- require_relative "relation/record_fetch_warning"
+ require "active_record/relation/record_fetch_warning"
end
end
end
@@ -146,12 +151,19 @@ end_warning
# Expose database runtime to controller for logging.
initializer "active_record.log_runtime" do
- require_relative "railties/controller_runtime"
+ require "active_record/railties/controller_runtime"
ActiveSupport.on_load(:action_controller) do
include ActiveRecord::Railties::ControllerRuntime
end
end
+ initializer "active_record.collection_cache_association_loading" do
+ require "active_record/railties/collection_cache_association_loading"
+ ActiveSupport.on_load(:action_view) do
+ ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading)
+ end
+ end
+
initializer "active_record.set_reloader_hooks" do
ActiveSupport.on_load(:active_record) do
ActiveSupport::Reloader.before_class_unload do
@@ -177,7 +189,16 @@ end_warning
initializer "active_record.clear_active_connections" do
config.after_initialize do
ActiveSupport.on_load(:active_record) do
+ # Ideally the application doesn't connect to the database during boot,
+ # but sometimes it does. In case it did, we want to empty out the
+ # connection pools so that a non-database-using process (e.g. a master
+ # process in a forking server model) doesn't retain a needless
+ # connection. If it was needed, the incremental cost of reestablishing
+ # this connection is trivial: the rest of the pool would need to be
+ # populated anyway.
+
clear_active_connections!
+ flush_idle_connections!
end
end
end
diff --git a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb
new file mode 100644
index 0000000000..b5129e4239
--- /dev/null
+++ b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Railties # :nodoc:
+ module CollectionCacheAssociationLoading #:nodoc:
+ def setup(context, options, block)
+ @relation = relation_from_options(options)
+
+ super
+ end
+
+ def relation_from_options(cached: nil, partial: nil, collection: nil, **_)
+ return unless cached
+
+ relation = partial if partial.is_a?(ActiveRecord::Relation)
+ relation ||= collection if collection.is_a?(ActiveRecord::Relation)
+
+ if relation && !relation.loaded?
+ relation.skip_preloading!
+ end
+ end
+
+ def collection_without_template
+ @relation.preload_associations(@collection) if @relation
+ super
+ end
+
+ def collection_with_template
+ @relation.preload_associations(@collection) if @relation
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb
index 3cf66980a5..309441a057 100644
--- a/activerecord/lib/active_record/railties/controller_runtime.rb
+++ b/activerecord/lib/active_record/railties/controller_runtime.rb
@@ -1,56 +1,51 @@
# frozen_string_literal: true
require "active_support/core_ext/module/attr_internal"
-require_relative "../log_subscriber"
+require "active_record/log_subscriber"
module ActiveRecord
module Railties # :nodoc:
module ControllerRuntime #:nodoc:
extend ActiveSupport::Concern
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_internal :db_runtime
-
- private
-
- def process_action(action, *args)
- # We also need to reset the runtime before each action
- # because of queries in middleware or in cases we are streaming
- # and it won't be cleaned up by the method below.
- ActiveRecord::LogSubscriber.reset_runtime
- super
+ module ClassMethods # :nodoc:
+ def log_process_action(payload)
+ messages, db_runtime = super, payload[:db_runtime]
+ messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime
+ messages
+ end
end
- def cleanup_view_runtime
- if logger && logger.info? && ActiveRecord::Base.connected?
- db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
- self.db_runtime = (db_runtime || 0) + db_rt_before_render
- runtime = super
- db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime
- self.db_runtime += db_rt_after_render
- runtime - db_rt_after_render
- else
+ private
+ attr_internal :db_runtime
+
+ def process_action(action, *args)
+ # We also need to reset the runtime before each action
+ # because of queries in middleware or in cases we are streaming
+ # and it won't be cleaned up by the method below.
+ ActiveRecord::LogSubscriber.reset_runtime
super
end
- end
- def append_info_to_payload(payload)
- super
- if ActiveRecord::Base.connected?
- payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime
+ def cleanup_view_runtime
+ if logger && logger.info? && ActiveRecord::Base.connected?
+ db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
+ self.db_runtime = (db_runtime || 0) + db_rt_before_render
+ runtime = super
+ db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime
+ self.db_runtime += db_rt_after_render
+ runtime - db_rt_after_render
+ else
+ super
+ end
end
- end
- module ClassMethods # :nodoc:
- def log_process_action(payload)
- messages, db_runtime = super, payload[:db_runtime]
- messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime
- messages
+ def append_info_to_payload(payload)
+ super
+ if ActiveRecord::Base.connected?
+ payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 691b3612d8..24449e8df3 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -4,16 +4,16 @@ require "active_record"
db_namespace = namespace :db do
desc "Set the environment value for the database"
- task "environment:set" => [:environment, :load_config] do
+ task "environment:set" => :load_config do
ActiveRecord::InternalMetadata.create_table
- ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
+ ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment
end
- task check_protected_environments: [:environment, :load_config] do
+ task check_protected_environments: :load_config do
ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
end
- task :load_config do
+ task load_config: :environment do
ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {}
ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
end
@@ -22,6 +22,14 @@ db_namespace = namespace :db do
task all: :load_config do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
+
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ desc "Create #{spec_name} database for current environment"
+ task spec_name => :load_config do
+ db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
+ ActiveRecord::Tasks::DatabaseTasks.create(db_config.config)
+ end
+ 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 or when RAILS_ENV is development, it defaults to creating the development and test databases."
@@ -33,6 +41,14 @@ db_namespace = namespace :db do
task all: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
+
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ desc "Drop #{spec_name} database for current environment"
+ task spec_name => [:load_config, :check_protected_environments] do
+ db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
+ ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config)
+ end
+ 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 or when RAILS_ENV is development, it defaults to dropping the development and test databases."
@@ -56,8 +72,11 @@ db_namespace = namespace :db do
end
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
- task migrate: [:environment, :load_config] do
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ task migrate: :load_config do
+ ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
+ ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
db_namespace["_dump"].invoke
end
@@ -77,8 +96,17 @@ db_namespace = namespace :db do
end
namespace :migrate do
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ desc "Migrate #{spec_name} database for current environment"
+ task spec_name => :load_config do
+ db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
+ end
+
# desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).'
- task redo: [:environment, :load_config] do
+ task redo: :load_config do
raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty?
if ENV["VERSION"]
@@ -94,24 +122,33 @@ db_namespace = namespace :db do
task reset: ["db:drop", "db:create", "db:migrate"]
# desc 'Runs the "up" for a given migration VERSION.'
- task up: [:environment, :load_config] do
+ task up: :load_config do
raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
- version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
- ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+
+ ActiveRecord::Base.connection.migration_context.run(
+ :up,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
db_namespace["_dump"].invoke
end
# desc 'Runs the "down" for a given migration VERSION.'
- task down: [:environment, :load_config] do
+ task down: :load_config do
raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty?
- version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
- ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
+
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+
+ ActiveRecord::Base.connection.migration_context.run(
+ :down,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
db_namespace["_dump"].invoke
end
desc "Display status of migrations"
- task status: [:environment, :load_config] do
+ task status: :load_config do
unless ActiveRecord::SchemaMigration.table_exists?
abort "Schema migrations table does not exist yet."
end
@@ -120,8 +157,7 @@ db_namespace = namespace :db do
puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n"
puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
puts "-" * 50
- paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
- ActiveRecord::Migrator.migrations_status(paths).each do |status, version, name|
+ ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name|
puts "#{status.center(8)} #{version.ljust(14)} #{name}"
end
puts
@@ -129,16 +165,16 @@ db_namespace = namespace :db do
end
desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)."
- task rollback: [:environment, :load_config] do
+ task rollback: :load_config do
step = ENV["STEP"] ? ENV["STEP"].to_i : 1
- ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step)
+ ActiveRecord::Base.connection.migration_context.rollback(step)
db_namespace["_dump"].invoke
end
# desc 'Pushes the schema to the next version (specify steps w/ STEP=n).'
- task forward: [:environment, :load_config] do
+ task forward: :load_config do
step = ENV["STEP"] ? ENV["STEP"].to_i : 1
- ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step)
+ ActiveRecord::Base.connection.migration_context.forward(step)
db_namespace["_dump"].invoke
end
@@ -146,12 +182,12 @@ db_namespace = namespace :db do
task reset: [ "db:drop", "db:setup" ]
# desc "Retrieves the charset for the current environment's database"
- task charset: [:environment, :load_config] do
+ task charset: :load_config do
puts ActiveRecord::Tasks::DatabaseTasks.charset_current
end
# desc "Retrieves the collation for the current environment's database"
- task collation: [:environment, :load_config] do
+ task collation: :load_config do
begin
puts ActiveRecord::Tasks::DatabaseTasks.collation_current
rescue NoMethodError
@@ -160,13 +196,13 @@ db_namespace = namespace :db do
end
desc "Retrieves the current schema version number"
- task version: [:environment, :load_config] do
- puts "Current version: #{ActiveRecord::Migrator.current_version}"
+ task version: :load_config do
+ puts "Current version: #{ActiveRecord::Base.connection.migration_context.current_version}"
end
# desc "Raises an error if there are pending migrations"
- task abort_if_pending_migrations: [:environment, :load_config] do
- pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations
+ task abort_if_pending_migrations: :load_config do
+ pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations
if pending_migrations.any?
puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}"
@@ -188,8 +224,8 @@ db_namespace = namespace :db do
namespace :fixtures do
desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
- task load: [:environment, :load_config] do
- require_relative "../fixtures"
+ task load: :load_config do
+ require "active_record/fixtures"
base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
@@ -210,8 +246,8 @@ db_namespace = namespace :db do
end
# desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
- task identify: [:environment, :load_config] do
- require_relative "../fixtures"
+ task identify: :load_config do
+ require "active_record/fixtures"
label, id = ENV["LABEL"], ENV["ID"]
raise "LABEL or ID required" if label.blank? && id.blank?
@@ -221,7 +257,7 @@ db_namespace = namespace :db do
base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
Dir["#{base_dir}/**/*.yml"].each do |file|
- if data = YAML::load(ERB.new(IO.read(file)).result)
+ if data = YAML.load(ERB.new(IO.read(file)).result)
data.each_key do |key|
key_id = ActiveRecord::FixtureSet.identify(key)
@@ -236,17 +272,22 @@ db_namespace = namespace :db do
namespace :schema do
desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record"
- task dump: [:environment, :load_config] do
- require_relative "../schema_dumper"
- filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema.rb")
- File.open(filename, "w:utf-8") do |file|
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
+ task dump: :load_config do
+ require "active_record/schema_dumper"
+
+ ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby)
+ File.open(filename, "w:utf-8") do |file|
+ ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
+ end
end
+
db_namespace["schema:dump"].reenable
end
desc "Loads a schema.rb file into the database"
- task load: [:environment, :load_config, :check_protected_environments] do
+ task load: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV["SCHEMA"])
end
@@ -256,39 +297,41 @@ db_namespace = namespace :db do
namespace :cache do
desc "Creates a db/schema_cache.yml file."
- task dump: [:environment, :load_config] do
+ task dump: :load_config do
conn = ActiveRecord::Base.connection
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml")
ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(conn, filename)
end
desc "Clears a db/schema_cache.yml file."
- task clear: [:environment, :load_config] do
+ task clear: :load_config do
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml")
rm_f filename, verbose: false
end
end
-
end
namespace :structure do
desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql"
- task dump: [:environment, :load_config] do
- filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
- current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
-
- if ActiveRecord::SchemaMigration.table_exists?
- File.open(filename, "a") do |f|
- f.puts ActiveRecord::Base.connection.dump_schema_information
- f.print "\n"
+ task dump: :load_config do
+ ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
+ ActiveRecord::Base.establish_connection(config)
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql)
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename)
+
+ if ActiveRecord::SchemaMigration.table_exists?
+ File.open(filename, "a") do |f|
+ f.puts ActiveRecord::Base.connection.dump_schema_information
+ f.print "\n"
+ end
end
end
+
db_namespace["structure:dump"].reenable
end
desc "Recreates the databases from the structure.sql file"
- task load: [:environment, :load_config, :check_protected_environments] do
+ task load: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV["SCHEMA"])
end
@@ -313,7 +356,7 @@ db_namespace = namespace :db do
begin
should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
ActiveRecord::Schema.verbose = false
- ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :ruby, ENV["SCHEMA"]
+ ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :ruby, ENV["SCHEMA"], "test"
ensure
if should_reconnect
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
@@ -323,16 +366,16 @@ db_namespace = namespace :db do
# desc "Recreate the test database from an existent structure.sql file"
task load_structure: %w(db:test:purge) do
- ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"]
+ ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"], "test"
end
# desc "Empty the test database"
- task purge: %w(environment load_config check_protected_environments) do
+ task purge: %w(load_config check_protected_environments) do
ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"]
end
# desc 'Load the test schema'
- task prepare: %w(environment load_config) do
+ task prepare: :load_config do
unless ActiveRecord::Base.configurations.blank?
db_namespace["test:load"].invoke
end
diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb
deleted file mode 100644
index 72c75ddd52..0000000000
--- a/activerecord/lib/active_record/railties/jdbcmysql_error.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-#FIXME Remove if ArJdbcMysql will give.
-module ArJdbcMySQL #:nodoc:
- class Error < StandardError #:nodoc:
- attr_accessor :error_number, :sql_state
-
- def initialize(msg)
- super
- @error_number = nil
- @sql_state = nil
- end
-
- # Mysql gem compatibility
- alias_method :errno, :error_number
- alias_method :error, :message
- end
-end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 82ab2415e1..2f43d005f3 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "active_support/core_ext/string/filters"
-require "active_support/deprecation"
require "concurrent/map"
module ActiveRecord
@@ -35,7 +34,8 @@ module ActiveRecord
def self.add_reflection(ar, name, reflection)
ar.clear_reflections_cache
- ar._reflections = ar._reflections.merge(name.to_s => reflection)
+ name = name.to_s
+ ar._reflections = ar._reflections.except(name).merge!(name => reflection)
end
def self.add_aggregate_reflection(ar, name, reflection)
@@ -174,11 +174,6 @@ module ActiveRecord
scope ? [scope] : []
end
- def scope_chain
- chain.map(&:scopes)
- end
- deprecate :scope_chain
-
def build_join_constraint(table, foreign_table)
key = join_keys.key
foreign_key = join_keys.foreign_key
@@ -198,7 +193,7 @@ module ActiveRecord
klass_scope = klass_join_scope(table, predicate_builder)
if type
- klass_scope.where!(type => foreign_klass.base_class.sti_name)
+ klass_scope.where!(type => foreign_klass.polymorphic_name)
end
scope_chain_items.inject(klass_scope, &:merge!)
@@ -296,10 +291,14 @@ module ActiveRecord
end
def build_scope(table, predicate_builder = predicate_builder(table))
- Relation.create(klass, table, predicate_builder)
+ Relation.create(
+ klass,
+ table: table,
+ predicate_builder: predicate_builder
+ )
end
- def join_primary_key(_)
+ def join_primary_key(*)
foreign_key
end
@@ -417,6 +416,9 @@ module ActiveRecord
# Active Record class.
class AssociationReflection < MacroReflection #:nodoc:
def compute_class(name)
+ if polymorphic?
+ raise ArgumentError, "Polymorphic association does not support to compute class."
+ end
active_record.send(:compute_type, name)
end
@@ -425,21 +427,13 @@ module ActiveRecord
def initialize(name, scope, options, active_record)
super
- @automatic_inverse_of = nil
@type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type")
- @foreign_type = options[:foreign_type] || "#{name}_type"
+ @foreign_type = options[:polymorphic] && (options[:foreign_type] || "#{name}_type")
@constructable = calculate_constructable(macro, options)
@association_scope_cache = Concurrent::Map.new
if options[:class_name] && options[:class_name].class == Class
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing a class to the `class_name` is deprecated and will raise
- an ArgumentError in Rails 5.2. It eagerloads more classes than
- necessary and potentially creates circular dependencies.
-
- Please pass the class name as a string:
- `#{macro} :#{name}, class_name: '#{options[:class_name]}'`
- MSG
+ raise ArgumentError, "A class was passed to `:class_name` but we are expecting a string."
end
end
@@ -472,10 +466,6 @@ module ActiveRecord
options[:primary_key] || primary_key(klass || self.klass)
end
- def association_primary_key_type
- klass.type_for_attribute(association_primary_key.to_s)
- end
-
def active_record_primary_key
@active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
end
@@ -581,7 +571,7 @@ module ActiveRecord
end
VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to]
- INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :foreign_key]
+ INVALID_AUTOMATIC_INVERSE_OPTIONS = [:through, :foreign_key]
def add_as_source(seed)
seed
@@ -609,12 +599,14 @@ module ActiveRecord
# If it cannot find a suitable inverse association name, it returns
# +nil+.
def inverse_name
- options.fetch(:inverse_of) do
- @automatic_inverse_of ||= automatic_inverse_of
+ unless defined?(@inverse_name)
+ @inverse_name = options.fetch(:inverse_of) { automatic_inverse_of }
end
+
+ @inverse_name
end
- # returns either false or the inverse association name that it finds.
+ # returns either +nil+ or the inverse association name that it finds.
def automatic_inverse_of
if can_find_inverse_of_automatically?(self)
inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym
@@ -631,17 +623,12 @@ module ActiveRecord
return inverse_name
end
end
-
- false
end
# Checks if the inverse reflection that is returned from the
# +automatic_inverse_of+ method is a valid reflection. We must
# make sure that the reflection's active_record name matches up
# with the current reflection's klass name.
- #
- # Note: klass will always be valid because when there's a NameError
- # from calling +klass+, +reflection+ will already be set to false.
def valid_inverse_reflection?(reflection)
reflection &&
klass <= reflection.active_record &&
@@ -736,7 +723,7 @@ module ActiveRecord
end
end
- def join_primary_key(klass)
+ def join_primary_key(klass = nil)
polymorphic? ? association_primary_key(klass) : association_primary_key
end
@@ -745,6 +732,9 @@ module ActiveRecord
end
private
+ def can_find_inverse_of_automatically?(_)
+ !polymorphic? && super
+ end
def calculate_constructable(macro, options)
!polymorphic?
@@ -853,10 +843,6 @@ module ActiveRecord
source_reflection.join_scopes(table, predicate_builder) + super
end
- def source_type_scope
- through_reflection.klass.where(foreign_type => options[:source_type])
- end
-
def has_scope?
scope || options[:source_type] ||
source_reflection.has_scope? ||
@@ -877,10 +863,6 @@ module ActiveRecord
actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass)
end
- def association_primary_key_type
- klass.type_for_attribute(association_primary_key.to_s)
- end
-
# Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
#
# class Post < ActiveRecord::Base
@@ -983,16 +965,14 @@ module ActiveRecord
collect_join_reflections(seed + [self])
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
protected
- attr_reader :delegate_reflection
-
def actual_source_reflection # FIXME: this is a horrible name
source_reflection.actual_source_reflection
end
private
+ attr_reader :delegate_reflection
+
def collect_join_reflections(seed)
a = source_reflection.add_as_source seed
if options[:source_type]
@@ -1016,29 +996,24 @@ module ActiveRecord
end
class PolymorphicReflection < AbstractReflection # :nodoc:
- delegate :klass, :scope, :plural_name, :type, :get_join_keys, to: :@reflection
+ delegate :klass, :scope, :plural_name, :type, :get_join_keys, :scope_for, to: :@reflection
def initialize(reflection, previous_reflection)
@reflection = reflection
@previous_reflection = previous_reflection
end
- def scopes
- scopes = @previous_reflection.scopes
- scopes << @previous_reflection.source_type_scope
- end
-
def join_scopes(table, predicate_builder) # :nodoc:
scopes = @previous_reflection.join_scopes(table, predicate_builder) + super
- scopes << @previous_reflection.source_type_scope
+ scopes << build_scope(table, predicate_builder).instance_exec(nil, &source_type_scope)
end
def constraints
- @reflection.constraints + [source_type_info]
+ @reflection.constraints + [source_type_scope]
end
private
- def source_type_info
+ def source_type_scope
type = @previous_reflection.foreign_type
source_type = @previous_reflection.options[:source_type]
lambda { |object| where(type => source_type) }
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index f6825e67d7..c055b97061 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -9,8 +9,9 @@ module ActiveRecord
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
:reverse_order, :distinct, :create_with, :skip_query_cache]
+
CLAUSE_METHODS = [:where, :having, :from]
- INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having]
+ INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having]
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS
@@ -18,17 +19,19 @@ module ActiveRecord
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
attr_reader :table, :klass, :loaded, :predicate_builder
+ attr_accessor :skip_preloading_value
alias :model :klass
alias :loaded? :loaded
alias :locked? :lock_value
- def initialize(klass, table, predicate_builder, values = {})
+ def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
@klass = klass
@table = table
@values = values
@offsets = {}
@loaded = false
@predicate_builder = predicate_builder
+ @delegate_to_klass = false
end
def initialize_copy(other)
@@ -36,67 +39,6 @@ module ActiveRecord
reset
end
- def insert(values) # :nodoc:
- primary_key_value = nil
-
- if primary_key && Hash === values
- primary_key_value = values[values.keys.find { |k|
- k.name == primary_key
- }]
-
- 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
-
- im = arel.create_insert
- im.into @table
-
- substitutes = substitute_values values
-
- if values.empty? # empty insert
- im.values = Arel.sql(connection.empty_insert_statement_value)
- else
- im.insert substitutes
- end
-
- @klass.connection.insert(
- im,
- "SQL",
- primary_key || false,
- primary_key_value,
- nil,
- )
- end
-
- def _update_record(values, id, id_was) # :nodoc:
- substitutes = substitute_values values
-
- scope = @klass.unscoped
-
- if @klass.finder_needs_type_condition?
- scope.unscope!(where: @klass.inheritance_column)
- end
-
- relation = scope.where(@klass.primary_key => (id_was || id))
- um = relation
- .arel
- .compile_update(substitutes, @klass.primary_key)
-
- @klass.connection.update(
- um,
- "SQL",
- )
- end
-
- def substitute_values(values) # :nodoc:
- values.map do |arel_attr, value|
- bind = predicate_builder.build_bind_attribute(arel_attr.name, value)
- [arel_attr, bind]
- end
- end
-
def arel_attribute(name) # :nodoc:
klass.arel_attribute(name, table)
end
@@ -113,8 +55,8 @@ module ActiveRecord
#
# user = users.new { |user| user.name = 'Oscar' }
# user.name # => Oscar
- def new(*args, &block)
- scoping { @klass.new(*args, &block) }
+ def new(attributes = nil, &block)
+ scoping { klass.new(scope_for_create(attributes), &block) }
end
alias build new
@@ -138,8 +80,12 @@ module ActiveRecord
#
# users.create(name: nil) # validation on name
# # => #<User id: nil, name: nil, ...>
- def create(*args, &block)
- scoping { @klass.create(*args, &block) }
+ def create(attributes = nil, &block)
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create(attr, &block) }
+ else
+ scoping { klass.create(scope_for_create(attributes), &block) }
+ end
end
# Similar to #create, but calls
@@ -148,8 +94,12 @@ module ActiveRecord
#
# Expects arguments in the same format as
# {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!].
- def create!(*args, &block)
- scoping { @klass.create!(*args, &block) }
+ def create!(attributes = nil, &block)
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create!(attr, &block) }
+ else
+ scoping { klass.create!(scope_for_create(attributes), &block) }
+ end
end
def first_or_create(attributes = nil, &block) # :nodoc:
@@ -195,23 +145,12 @@ module ActiveRecord
# failed due to validation errors it won't be persisted, you get what
# #create returns in such situation.
#
- # Please note *this method is not atomic*, it runs first a SELECT, and if
+ # Please note <b>this method is not atomic</b>, it runs first a SELECT, and if
# there are no results an INSERT is attempted. If there are other threads
# or processes there is a race condition between both calls and it could
# be the case that you end up with two similar records.
#
- # Whether that is a problem or not depends on the logic of the
- # application, but in the particular case in which rows have a UNIQUE
- # constraint an exception may be raised, just retry:
- #
- # begin
- # CreditAccount.transaction(requires_new: true) do
- # CreditAccount.find_or_create_by(user_id: user.id)
- # end
- # rescue ActiveRecord::RecordNotUnique
- # retry
- # end
- #
+ # If this might be a problem for your application, please see #create_or_find_by.
def find_or_create_by(attributes, &block)
find_by(attributes) || create(attributes, &block)
end
@@ -223,6 +162,47 @@ module ActiveRecord
find_by(attributes) || create!(attributes, &block)
end
+ # Attempts to create a record with the given attributes in a table that has a unique constraint
+ # on one or several of its columns. If a row already exists with one or several of these
+ # unique constraints, the exception such an insertion would normally raise is caught,
+ # and the existing record with those attributes is found using #find_by.
+ #
+ # This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT
+ # and the INSERT, as that method needs to first query the table, then attempt to insert a row
+ # if none is found.
+ #
+ # There are several drawbacks to #create_or_find_by, though:
+ #
+ # * The underlying table must have the relevant columns defined with unique constraints.
+ # * A unique constraint violation may be triggered by only one, or at least less than all,
+ # of the given attributes. This means that the subsequent #find_by may fail to find a
+ # matching record, which will then raise an <tt>ActiveRecord::RecordNotFound</tt> exception,
+ # rather than a record with the given attributes.
+ # * While we avoid the race condition between SELECT -> INSERT from #find_or_create_by,
+ # we actually have another race condition between INSERT -> SELECT, which can be triggered
+ # if a DELETE between those two statements is run by another client. But for most applications,
+ # that's a significantly less likely condition to hit.
+ # * It relies on exception handling to handle control flow, which may be marginally slower.
+ #
+ # This method will return a record if all given attributes are covered by unique constraints
+ # (unless the INSERT -> DELETE -> SELECT race condition is triggered), but if creation was attempted
+ # and failed due to validation errors it won't be persisted, you get what #create returns in
+ # such situation.
+ def create_or_find_by(attributes, &block)
+ transaction(requires_new: true) { create(attributes, &block) }
+ rescue ActiveRecord::RecordNotUnique
+ find_by!(attributes)
+ end
+
+ # Like #create_or_find_by, but calls
+ # {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception
+ # is raised if the created record is invalid.
+ def create_or_find_by!(attributes, &block)
+ transaction(requires_new: true) { create!(attributes, &block) }
+ rescue ActiveRecord::RecordNotUnique
+ find_by!(attributes)
+ end
+
# Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new]
# instead of {create}[rdoc-ref:Persistence::ClassMethods#create].
def find_or_initialize_by(attributes, &block)
@@ -243,9 +223,10 @@ module ActiveRecord
end
# Converts relation objects to Array.
- def to_a
+ def to_ary
records.dup
end
+ alias to_a to_ary
def records # :nodoc:
load
@@ -334,6 +315,13 @@ module ActiveRecord
klass.current_scope = previous
end
+ def _exec_scope(*args, &block) # :nodoc:
+ @delegate_to_klass = true
+ instance_exec(*args, &block) || self
+ ensure
+ @delegate_to_klass = false
+ end
+
# Updates all records in the current relation with details given. This method constructs a single SQL UPDATE
# statement and sends it straight to the database. It does not instantiate the involved models and it does not
# trigger Active Record callbacks or validations. However, values passed to #update_all will still go through
@@ -359,12 +347,17 @@ module ActiveRecord
def update_all(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
+ if eager_loading?
+ relation = apply_join_dependency
+ return relation.update_all(updates)
+ end
+
stmt = Arel::UpdateManager.new
- stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
+ stmt.set Arel.sql(@klass.sanitize_sql_for_assignment(updates))
stmt.table(table)
- if has_join_values?
+ if has_join_values? || offset_value
@klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key))
else
stmt.key = arel_attribute(primary_key)
@@ -373,7 +366,44 @@ module ActiveRecord
stmt.wheres = arel.constraints
end
- @klass.connection.update stmt, "SQL"
+ @klass.connection.update stmt, "#{@klass} Update All"
+ end
+
+ # Touches all records in the current relation without instantiating records first with the updated_at/on attributes
+ # set to the current time or the time specified.
+ # This method can be passed attribute names and an optional time argument.
+ # If attribute names are passed, they are updated along with updated_at/on attributes.
+ # If no time argument is passed, the current time is used as default.
+ #
+ # === Examples
+ #
+ # # Touch all records
+ # Person.all.touch_all
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670'"
+ #
+ # # Touch multiple records with a custom attribute
+ # Person.all.touch_all(:created_at)
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670', \"created_at\" = '2018-01-04 22:55:23.132670'"
+ #
+ # # Touch multiple records with a specified time
+ # Person.all.touch_all(time: Time.new(2020, 5, 16, 0, 0, 0))
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2020-05-16 00:00:00'"
+ #
+ # # Touch records with scope
+ # Person.where(name: 'David').touch_all
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'"
+ def touch_all(*names, time: nil)
+ attributes = Array(names) + klass.timestamp_attributes_for_update_in_model
+ time ||= klass.current_time_from_proper_timezone
+ updates = {}
+ attributes.each { |column| updates[column] = time }
+
+ if klass.locking_enabled?
+ quoted_locking_column = connection.quote_column_name(klass.locking_column)
+ updates = sanitize_sql_for_assignment(updates) + ", #{quoted_locking_column} = COALESCE(#{quoted_locking_column}, 0) + 1"
+ end
+
+ update_all(updates)
end
# Destroys the records by instantiating each
@@ -412,8 +442,8 @@ module ActiveRecord
#
# If an invalid method is supplied, #delete_all raises an ActiveRecordError:
#
- # Post.limit(100).delete_all
- # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
+ # Post.distinct.delete_all
+ # # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct
def delete_all
invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
value = get_value(method)
@@ -423,16 +453,21 @@ module ActiveRecord
raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
end
+ if eager_loading?
+ relation = apply_join_dependency
+ return relation.delete_all
+ end
+
stmt = Arel::DeleteManager.new
stmt.from(table)
- if has_join_values?
+ if has_join_values? || has_limit_or_offset?
@klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key))
else
stmt.wheres = arel.constraints
end
- affected = @klass.connection.delete(stmt, "SQL")
+ affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
reset
affected
@@ -457,6 +492,7 @@ module ActiveRecord
end
def reset
+ @delegate_to_klass = false
@to_sql = @arel = @loaded = @should_eager_load = nil
@records = [].freeze
@offsets = {}
@@ -472,7 +508,7 @@ module ActiveRecord
relation = self
if eager_loading?
- find_with_associations { |rel, _| relation = rel }
+ apply_join_dependency { |rel, _| relation = rel }
end
conn = klass.connection
@@ -490,8 +526,10 @@ module ActiveRecord
where_clause.to_h(relation_table_name)
end
- def scope_for_create
- where_values_hash.merge!(create_with_value.stringify_keys)
+ def scope_for_create(attributes = nil)
+ scope = where_values_hash.merge!(create_with_value.stringify_keys)
+ scope.merge!(attributes) if attributes
+ scope
end
# Returns true if relation needs eager loading.
@@ -551,6 +589,21 @@ module ActiveRecord
limit_value || offset_value
end
+ def alias_tracker(joins = [], aliases = nil) # :nodoc:
+ joins += [aliases] if aliases
+ ActiveRecord::Associations::AliasTracker.create(connection, table.name, joins)
+ end
+
+ def preload_associations(records) # :nodoc:
+ preload = preload_values
+ preload += includes_values unless eager_loading?
+ preloader = nil
+ preload.each do |associations|
+ preloader ||= build_preloader
+ preloader.preload records, associations
+ end
+ end
+
protected
def load_records(records)
@@ -568,7 +621,7 @@ module ActiveRecord
skip_query_cache_if_necessary do
@records =
if eager_loading?
- find_with_associations do |relation, join_dependency|
+ apply_join_dependency do |relation, join_dependency|
if ActiveRecord::NullRelation === relation
[]
else
@@ -580,13 +633,7 @@ module ActiveRecord
klass.find_by_sql(arel, &block).freeze
end
- preload = preload_values
- preload += includes_values unless eager_loading?
- preloader = nil
- preload.each do |associations|
- preloader ||= build_preloader
- preloader.preload @records, associations
- end
+ preload_associations(@records) unless skip_preloading_value
@records.each(&:readonly!) if readonly_value
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index 356ad0dcd6..ec4bb06c57 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "batches/batch_enumerator"
+require "active_record/relation/batches/batch_enumerator"
module ActiveRecord
module Batches
@@ -251,25 +251,31 @@ module ActiveRecord
end
end
- attr = Relation::QueryAttribute.new(primary_key, primary_key_offset, klass.type_for_attribute(primary_key))
- batch_relation = relation.where(arel_attribute(primary_key).gt(Arel::Nodes::BindParam.new(attr)))
+ bind = primary_key_bind(primary_key_offset)
+ batch_relation = relation.where(arel_attribute(primary_key).gt(bind))
end
end
private
def apply_limits(relation, start, finish)
- if start
- attr = Relation::QueryAttribute.new(primary_key, start, klass.type_for_attribute(primary_key))
- relation = relation.where(arel_attribute(primary_key).gteq(Arel::Nodes::BindParam.new(attr)))
- end
- if finish
- attr = Relation::QueryAttribute.new(primary_key, finish, klass.type_for_attribute(primary_key))
- relation = relation.where(arel_attribute(primary_key).lteq(Arel::Nodes::BindParam.new(attr)))
- end
+ relation = apply_start_limit(relation, start) if start
+ relation = apply_finish_limit(relation, finish) if finish
relation
end
+ def apply_start_limit(relation, start)
+ relation.where(arel_attribute(primary_key).gteq(primary_key_bind(start)))
+ end
+
+ def apply_finish_limit(relation, finish)
+ relation.where(arel_attribute(primary_key).lteq(primary_key_bind(finish)))
+ end
+
+ def primary_key_bind(value)
+ predicate_builder.build_bind_attribute(primary_key, value)
+ end
+
def batch_order
arel_attribute(primary_key).asc
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 42d43224fa..f215c95f51 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -130,8 +130,15 @@ module ActiveRecord
# end
def calculate(operation, column_name)
if has_include?(column_name)
- relation = construct_relation_for_association_calculations
- relation.distinct! if operation.to_s.downcase == "count"
+ relation = apply_join_dependency
+
+ if operation.to_s.downcase == "count"
+ relation.distinct!
+ # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
+ if (column_name == :all || column_name.nil?) && select_values.empty?
+ relation.order_values = []
+ end
+ end
relation.calculate(operation, column_name)
else
@@ -180,8 +187,10 @@ module ActiveRecord
end
if has_include?(column_names.first)
- construct_relation_for_association_calculations.pluck(*column_names)
+ relation = apply_join_dependency
+ relation.pluck(*column_names)
else
+ enforce_raw_sql_whitelist(column_names)
relation = spawn
relation.select_values = column_names.map { |cn|
@klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn
@@ -191,6 +200,24 @@ module ActiveRecord
end
end
+ # Pick the value(s) from the named column(s) in the current relation.
+ # This is short-hand for <tt>relation.limit(1).pluck(*column_names).first</tt>, and is primarily useful
+ # when you have a relation that's already narrowed down to a single row.
+ #
+ # Just like #pluck, #pick will only load the actual value, not the entire record object, so it's also
+ # more efficient. The value is, again like with pluck, typecast by the column type.
+ #
+ # Person.where(id: 1).pick(:name)
+ # # SELECT people.name FROM people WHERE id = 1 LIMIT 1
+ # # => 'David'
+ #
+ # Person.where(id: 1).pick(:name, :email_address)
+ # # SELECT people.name, people.email_address FROM people WHERE id = 1 LIMIT 1
+ # # => [ 'David', 'david@loudthinking.com' ]
+ def pick(*column_names)
+ limit(1).pluck(*column_names).first
+ end
+
# Pluck all the ID's for the relation using the table's primary key
#
# Person.ids # SELECT people.id FROM people
@@ -215,7 +242,7 @@ module ActiveRecord
if operation == "count"
column_name ||= select_for_count
if column_name == :all
- if distinct && !(has_limit_or_offset? && order_values.any?)
+ if distinct && (group_values.any? || select_values.empty? && order_values.empty?)
column_name = primary_key
end
elsif column_name =~ /\s*DISTINCT[\s(]+/i
@@ -233,7 +260,7 @@ module ActiveRecord
def aggregate_column(column_name)
return column_name if Arel::Expressions === column_name
- if @klass.has_attribute?(column_name.to_s) || @klass.attribute_alias?(column_name.to_s)
+ if @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name)
@klass.arel_attribute(column_name)
else
Arel.sql(column_name == :all ? "*" : column_name.to_s)
@@ -247,7 +274,7 @@ module ActiveRecord
def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
column_alias = column_name
- if operation == "count" && has_limit_or_offset?
+ if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?)
# Shortcut when limit is zero.
return 0 if limit_value == 0
@@ -389,14 +416,12 @@ module ActiveRecord
end
def build_count_subquery(relation, column_name, distinct)
- relation.select_values = [
- if column_name == :all
- distinct ? table[Arel.star] : Arel.sql("1")
- else
- column_alias = Arel.sql("count_column")
- aggregate_column(column_name).as(column_alias)
- end
- ]
+ if column_name == :all
+ relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct
+ else
+ column_alias = Arel.sql("count_column")
+ relation.select_values = [ aggregate_column(column_name).as(column_alias) ]
+ end
subquery = relation.arel.as(Arel.sql("subquery_for_count"))
select_value = operation_over_aggregate_column(column_alias || Arel.star, "count", false)
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 48af777b69..488f71cdde 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -38,7 +38,7 @@ module ActiveRecord
# may vary depending on the klass of a relation, so we create a subclass of Relation
# for each different klass, and the delegations are compiled into that subclass only.
- delegate :to_xml, :encode_with, :length, :each, :uniq, :to_ary, :join,
+ delegate :to_xml, :encode_with, :length, :each, :join,
:[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of,
:to_sentence, :to_formatted_s, :as_json,
:shuffle, :split, :slice, :index, :rindex, to: :records
@@ -82,6 +82,11 @@ module ActiveRecord
if @klass.respond_to?(method)
self.class.delegate_to_scoped_klass(method)
scoping { @klass.public_send(method, *args, &block) }
+ elsif @delegate_to_klass && @klass.respond_to?(method, true)
+ ActiveSupport::Deprecation.warn \
+ "Delegating missing #{method} method to #{@klass}. " \
+ "Accessibility of private/protected class methods in :scope is deprecated and will be removed in Rails 6.0."
+ @klass.send(method, *args, &block)
elsif arel.respond_to?(method)
ActiveSupport::Deprecation.warn \
"Delegating #{method} to arel is deprecated and will be removed in Rails 6.0."
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index c92d5a52f4..f7613a187d 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -18,9 +18,10 @@ module ActiveRecord
# Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
- # NOTE: The returned records may not be in the same order as the ids you
- # provide since database rows are unordered. You will need to provide an explicit QueryMethods#order
- # option if you want the results to be sorted.
+ # NOTE: The returned records are in the same order as the ids you provide.
+ # If you want the results to be sorted by database, you can use ActiveRecord::QueryMethods#where
+ # method and provide an explicit ActiveRecord::QueryMethods#order option.
+ # But ActiveRecord::QueryMethods#where method doesn't raise ActiveRecord::RecordNotFound.
#
# ==== Find with lock
#
@@ -88,7 +89,7 @@ module ActiveRecord
where(arg, *args).take!
rescue ::RangeError
raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value",
- @klass.name)
+ @klass.name, @klass.primary_key)
end
# Gives a record (or N records if a parameter is supplied) without any implied
@@ -147,7 +148,7 @@ module ActiveRecord
#
# [#<Person id:4>, #<Person id:3>, #<Person id:2>]
def last(limit = nil)
- return find_last(limit) if loaded? || limit_value
+ return find_last(limit) if loaded? || has_limit_or_offset?
result = ordered_relation.limit(limit)
result = result.reverse_order!
@@ -284,7 +285,7 @@ module ActiveRecord
# * Hash - Finds the record that matches these +find+-style conditions
# (such as <tt>{name: 'David'}</tt>).
# * +false+ - Returns always +false+.
- # * No args - Returns +false+ if the table is empty, +true+ otherwise.
+ # * No args - Returns +false+ if the relation is empty, +true+ otherwise.
#
# For more information about specifying conditions as a hash or array,
# see the Conditions section in the introduction to ActiveRecord::Base.
@@ -300,6 +301,7 @@ module ActiveRecord
# Person.exists?(name: 'David')
# Person.exists?(false)
# Person.exists?
+ # Person.where(name: 'Spartacus', rating: 4).exists?
def exists?(conditions = :none)
if Base === conditions
raise ArgumentError, <<-MSG.squish
@@ -310,12 +312,12 @@ module ActiveRecord
return false if !conditions || limit_value == 0
- relation = self unless eager_loading?
- relation ||= apply_join_dependency(self, construct_join_dependency(eager_loading: false))
-
- return false if ActiveRecord::NullRelation === relation
+ if eager_loading?
+ relation = apply_join_dependency(eager_loading: false)
+ return relation.exists?(conditions)
+ end
- relation = construct_relation_for_exists(relation, conditions)
+ relation = construct_relation_for_exists(conditions)
skip_query_cache_if_necessary { connection.select_value(relation.arel, "#{name} Exists") } ? true : false
rescue ::RangeError
@@ -338,7 +340,7 @@ module ActiveRecord
if ids.nil?
error = "Couldn't find #{name}".dup
error << " with#{conditions}" if conditions
- raise RecordNotFound.new(error, name)
+ raise RecordNotFound.new(error, name, key)
elsif Array(ids).size == 1
error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}"
raise RecordNotFound.new(error, name, key, ids)
@@ -346,7 +348,7 @@ module ActiveRecord
error = "Couldn't find all #{name.pluralize} with '#{key}': ".dup
error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})."
error << " Couldn't find #{name.pluralize(not_found_ids.size)} with #{key.to_s.pluralize(not_found_ids.size)} #{not_found_ids.join(', ')}." if not_found_ids
- raise RecordNotFound.new(error, name, primary_key, ids)
+ raise RecordNotFound.new(error, name, key, ids)
end
end
@@ -356,27 +358,8 @@ module ActiveRecord
offset_value || 0
end
- def find_with_associations
- # NOTE: the JoinDependency constructed here needs to know about
- # any joins already present in `self`, so pass them in
- #
- # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136
- # incorrect SQL is generated. In that case, the join dependency for
- # SpecialCategorizations is constructed without knowledge of the
- # preexisting join in joins_values to categorizations (by way of
- # the `has_many :through` for categories).
- #
- join_dependency = construct_join_dependency(joins_values)
-
- aliases = join_dependency.aliases
- relation = select aliases.columns
- relation = apply_join_dependency(relation, join_dependency)
-
- yield relation, join_dependency
- end
-
- def construct_relation_for_exists(relation, conditions)
- relation = relation.except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
+ def construct_relation_for_exists(conditions)
+ relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
case conditions
when Array, Hash
@@ -388,26 +371,31 @@ module ActiveRecord
relation
end
- def construct_join_dependency(joins = [], eager_loading: true)
+ def construct_join_dependency
including = eager_load_values + includes_values
- ActiveRecord::Associations::JoinDependency.new(klass, table, including, joins, eager_loading: eager_loading)
- end
-
- def construct_relation_for_association_calculations
- apply_join_dependency(self, construct_join_dependency(joins_values))
+ joins = joins_values.select { |join| join.is_a?(Arel::Nodes::Join) }
+ ActiveRecord::Associations::JoinDependency.new(
+ klass, table, including, alias_tracker(joins)
+ )
end
- def apply_join_dependency(relation, join_dependency)
- relation = relation.except(:includes, :eager_load, :preload).joins!(join_dependency)
+ def apply_join_dependency(eager_loading: true)
+ join_dependency = construct_join_dependency
+ relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
- if using_limitable_reflections?(join_dependency.reflections)
- relation
- else
- if relation.limit_value
+ if eager_loading && !using_limitable_reflections?(join_dependency.reflections)
+ if has_limit_or_offset?
limited_ids = limited_ids_for(relation)
limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids)
end
- relation.except(:limit, :offset)
+ relation.limit_value = relation.offset_value = nil
+ end
+
+ if block_given?
+ relation._select!(join_dependency.aliases.columns)
+ yield relation, join_dependency
+ else
+ relation
end
end
@@ -435,9 +423,12 @@ module ActiveRecord
ids = ids.flatten.compact.uniq
+ model_name = @klass.name
+
case ids.size
when 0
- raise RecordNotFound, "Couldn't find #{@klass.name} without an ID"
+ error_message = "Couldn't find #{model_name} without an ID"
+ raise RecordNotFound.new(error_message, model_name, primary_key)
when 1
result = find_one(ids.first)
expects_array ? [ result ] : result
@@ -445,7 +436,8 @@ module ActiveRecord
find_some(ids)
end
rescue ::RangeError
- raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID"
+ error_message = "Couldn't find #{model_name} with an out of range ID"
+ raise RecordNotFound.new(error_message, model_name, primary_key, ids)
end
def find_one(id)
@@ -529,7 +521,11 @@ module ActiveRecord
else
relation = ordered_relation
- if limit_value.nil? || index < limit_value
+ if limit_value
+ limit = [limit_value - index, limit].min
+ end
+
+ if limit > 0
relation = relation.offset(offset_index + index) unless index.zero?
relation.limit(limit).to_a
else
@@ -544,12 +540,11 @@ module ActiveRecord
else
relation = ordered_relation
- 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
+ if equal?(relation) || has_limit_or_offset?
+ relation.records[-index]
+ else
+ relation.last(index)[-index]
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 03824ffff9..25510d4a57 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -23,7 +23,11 @@ module ActiveRecord
# build a relation to merge in rather than directly merging
# the values.
def other
- other = Relation.create(relation.klass, relation.table, relation.predicate_builder)
+ other = Relation.create(
+ relation.klass,
+ table: relation.table,
+ predicate_builder: relation.predicate_builder
+ )
hash.each { |k, v|
if k == :joins
if Hash === v
@@ -52,7 +56,7 @@ module ActiveRecord
NORMAL_VALUES = Relation::VALUE_METHODS -
Relation::CLAUSE_METHODS -
- [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
+ [:includes, :preload, :joins, :left_outer_joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
def normal_values
NORMAL_VALUES
@@ -79,6 +83,7 @@ module ActiveRecord
merge_clauses
merge_preloads
merge_joins
+ merge_outer_joins
relation
end
@@ -112,22 +117,43 @@ module ActiveRecord
if other.klass == relation.klass
relation.joins!(*other.joins_values)
else
- joins_dependency, rest = other.joins_values.partition do |join|
+ alias_tracker = nil
+ joins_dependency = other.joins_values.map do |join|
case join
when Hash, Symbol, Array
- true
+ alias_tracker ||= other.alias_tracker
+ ActiveRecord::Associations::JoinDependency.new(
+ other.klass, other.table, join, alias_tracker
+ )
else
- false
+ join
end
end
- join_dependency = ActiveRecord::Associations::JoinDependency.new(
- other.klass, other.table, joins_dependency, []
- )
+ relation.joins!(*joins_dependency)
+ end
+ end
+
+ def merge_outer_joins
+ return if other.left_outer_joins_values.blank?
- relation.joins! rest
+ if other.klass == relation.klass
+ relation.left_outer_joins!(*other.left_outer_joins_values)
+ else
+ alias_tracker = nil
+ joins_dependency = other.left_outer_joins_values.map do |join|
+ case join
+ when Hash, Symbol, Array
+ alias_tracker ||= other.alias_tracker
+ ActiveRecord::Associations::JoinDependency.new(
+ other.klass, other.table, join, alias_tracker
+ )
+ else
+ join
+ end
+ end
- @relation = relation.joins join_dependency
+ relation.left_outer_joins!(*joins_dependency)
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 5c42414072..7a0edcbc33 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -13,6 +13,7 @@ module ActiveRecord
register_handler(Range, RangeHandler.new(self))
register_handler(Relation, RelationHandler.new)
register_handler(Array, ArrayHandler.new(self))
+ register_handler(Set, ArrayHandler.new(self))
end
def build_from_hash(attributes)
@@ -56,9 +57,6 @@ module ActiveRecord
end
protected
-
- attr_reader :table
-
def expand_from_hash(attributes)
return ["1=0"] if attributes.empty?
@@ -85,6 +83,18 @@ module ActiveRecord
expand_from_hash(query).reduce(&:and)
end
queries.reduce(&:or)
+ elsif table.aggregated_with?(key)
+ mapping = table.reflect_on_aggregation(key).mapping
+ queries = Array.wrap(value).map do |object|
+ mapping.map do |field_attr, aggregate_attr|
+ if mapping.size == 1 && !object.respond_to?(aggregate_attr)
+ build(table.arel_attribute(field_attr), object)
+ else
+ build(table.arel_attribute(field_attr), object.send(aggregate_attr))
+ end
+ end.reduce(&:and)
+ end
+ queries.reduce(&:or)
# FIXME: Deprecate this and provide a public API to force equality
elsif (value.is_a?(Range) || value.is_a?(Array)) &&
table.type(key.to_s).respond_to?(:subtype)
@@ -96,6 +106,7 @@ module ActiveRecord
end
private
+ attr_reader :table
def associated_predicate_builder(association_name)
self.class.new(table.associated_table(association_name))
@@ -123,11 +134,11 @@ module ActiveRecord
end
end
-require_relative "predicate_builder/array_handler"
-require_relative "predicate_builder/base_handler"
-require_relative "predicate_builder/basic_object_handler"
-require_relative "predicate_builder/range_handler"
-require_relative "predicate_builder/relation_handler"
+require "active_record/relation/predicate_builder/array_handler"
+require "active_record/relation/predicate_builder/base_handler"
+require "active_record/relation/predicate_builder/basic_object_handler"
+require "active_record/relation/predicate_builder/range_handler"
+require "active_record/relation/predicate_builder/relation_handler"
-require_relative "predicate_builder/association_query_value"
-require_relative "predicate_builder/polymorphic_array_value"
+require "active_record/relation/predicate_builder/association_query_value"
+require "active_record/relation/predicate_builder/polymorphic_array_value"
diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
index 2fd75c8958..64bf83e3c1 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -34,8 +34,7 @@ module ActiveRecord
array_predicates.inject(&:or)
end
- protected
-
+ private
attr_reader :predicate_builder
module NullPredicate # :nodoc:
diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb
index 0255a65bfe..88cd71cf69 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb
@@ -12,12 +12,9 @@ module ActiveRecord
[associated_table.association_join_foreign_key.to_s => ids]
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ private
attr_reader :associated_table, :value
- private
def ids
case value
when Relation
@@ -30,7 +27,7 @@ module ActiveRecord
end
def primary_key
- associated_table.association_join_keys.key
+ associated_table.association_join_primary_key
end
def convert_to_id(value)
diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
index 112821135f..10c5c1a66a 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
@@ -11,8 +11,7 @@ module ActiveRecord
predicate_builder.build(attribute, value.id)
end
- protected
-
+ private
attr_reader :predicate_builder
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
index 34db266f05..e8c9f60860 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
@@ -12,8 +12,7 @@ module ActiveRecord
attribute.eq(bind)
end
- protected
-
+ private
attr_reader :predicate_builder
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb
index b87b5c36dd..aae04d9348 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb
@@ -17,27 +17,26 @@ module ActiveRecord
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ private
attr_reader :associated_table, :values
- private
def type_to_ids_mapping
default_hash = Hash.new { |hsh, key| hsh[key] = [] }
- values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) }
+ values.each_with_object(default_hash) do |value, hash|
+ hash[klass(value).polymorphic_name] << convert_to_id(value)
+ end
end
def primary_key(value)
- associated_table.association_primary_key(base_class(value))
+ associated_table.association_join_primary_key(klass(value))
end
- def base_class(value)
+ def klass(value)
case value
when Base
- value.class.base_class
+ value.class
when Relation
- value.klass.base_class
+ value.klass
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
index 6d16579708..44bb2c7ab6 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
@@ -16,15 +16,16 @@ module ActiveRecord
def call(attribute, value)
begin_bind = predicate_builder.build_bind_attribute(attribute.name, value.begin)
end_bind = predicate_builder.build_bind_attribute(attribute.name, value.end)
- if value.begin.respond_to?(:infinite?) && value.begin.infinite?
- if value.end.respond_to?(:infinite?) && value.end.infinite?
+
+ if begin_bind.value.infinity?
+ if end_bind.value.infinity?
attribute.not_in([])
elsif value.exclude_end?
attribute.lt(end_bind)
else
attribute.lteq(end_bind)
end
- elsif value.end.respond_to?(:infinite?) && value.end.infinite?
+ elsif end_bind.value.infinity?
attribute.gteq(begin_bind)
elsif value.exclude_end?
attribute.gteq(begin_bind).and(attribute.lt(end_bind))
@@ -33,8 +34,7 @@ module ActiveRecord
end
end
- protected
-
+ private
attr_reader :predicate_builder
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
index f51ea4fde0..c8bbfa5051 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
@@ -4,6 +4,10 @@ module ActiveRecord
class PredicateBuilder
class RelationHandler # :nodoc:
def call(attribute, value)
+ if value.eager_loading?
+ value = value.send(:apply_join_dependency)
+ end
+
if value.select_values.empty?
value = value.select(value.arel_attribute(value.klass.primary_key))
end
diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb
index 5a9a7fd432..f64bd30d38 100644
--- a/activerecord/lib/active_record/relation/query_attribute.rb
+++ b/activerecord/lib/active_record/relation/query_attribute.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
-require_relative "../attribute"
+require "active_model/attribute"
module ActiveRecord
class Relation
- class QueryAttribute < Attribute # :nodoc:
+ class QueryAttribute < ActiveModel::Attribute # :nodoc:
def type_cast(value)
value
end
@@ -21,6 +21,23 @@ module ActiveRecord
!value_before_type_cast.is_a?(StatementCache::Substitute) &&
(value_before_type_cast.nil? || value_for_database.nil?)
end
+
+ def boundable?
+ return @_boundable if defined?(@_boundable)
+ nil?
+ @_boundable = true
+ rescue ::RangeError
+ @_boundable = false
+ end
+
+ def infinity?
+ _infinity?(value_before_type_cast) || boundable? && _infinity?(value_for_database)
+ end
+
+ private
+ def _infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index c88603fde2..db9101a168 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-require_relative "from_clause"
-require_relative "query_attribute"
-require_relative "where_clause"
-require_relative "where_clause_factory"
+require "active_record/relation/from_clause"
+require "active_record/relation/query_attribute"
+require "active_record/relation/where_clause"
+require "active_record/relation/where_clause_factory"
require "active_model/forbidden_attributes_protection"
module ActiveRecord
@@ -231,6 +231,7 @@ module ActiveRecord
end
def _select!(*fields) # :nodoc:
+ fields.reject!(&:blank?)
fields.flatten!
fields.map! do |field|
klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
@@ -295,6 +296,7 @@ module ActiveRecord
spawn.order!(*args)
end
+ # Same as #order but operates on relation in-place instead of copying.
def order!(*args) # :nodoc:
preprocess_order_args(args)
@@ -316,6 +318,7 @@ module ActiveRecord
spawn.reorder!(*args)
end
+ # Same as #reorder but operates on relation in-place instead of copying.
def reorder!(*args) # :nodoc:
preprocess_order_args(args)
@@ -325,8 +328,8 @@ module ActiveRecord
end
VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
- :limit, :offset, :joins, :includes, :from,
- :readonly, :having])
+ :limit, :offset, :joins, :left_outer_joins,
+ :includes, :from, :readonly, :having])
# Removes an unwanted relation that is already defined on a chain of relations.
# This is useful when passing around chains of relations and would like to
@@ -373,10 +376,11 @@ module ActiveRecord
args.each do |scope|
case scope
when Symbol
+ scope = :left_outer_joins if scope == :left_joins
if !VALID_UNSCOPING_VALUES.include?(scope)
raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
end
- set_value(scope, nil)
+ set_value(scope, DEFAULT_VALUES[scope])
when Hash
scope.each do |key, target_value|
if key != :where
@@ -441,16 +445,14 @@ module ActiveRecord
# => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
#
def left_outer_joins(*args)
- check_if_method_has_arguments!(:left_outer_joins, args)
-
- args.compact!
- args.flatten!
-
+ check_if_method_has_arguments!(__callee__, args)
spawn.left_outer_joins!(*args)
end
alias :left_joins :left_outer_joins
def left_outer_joins!(*args) # :nodoc:
+ args.compact!
+ args.flatten!
self.left_outer_joins_values += args
self
end
@@ -892,21 +894,27 @@ module ActiveRecord
self
end
- def skip_query_cache! # :nodoc:
- self.skip_query_cache_value = true
+ def skip_query_cache!(value = true) # :nodoc:
+ self.skip_query_cache_value = value
+ self
+ end
+
+ def skip_preloading! # :nodoc:
+ self.skip_preloading_value = true
self
end
# Returns the Arel object associated with the relation.
- def arel # :nodoc:
- @arel ||= build_arel
+ def arel(aliases = nil) # :nodoc:
+ @arel ||= build_arel(aliases)
+ end
+
+ # Returns a relation value with a given name
+ def get_value(name) # :nodoc:
+ @values.fetch(name, DEFAULT_VALUES[name])
end
protected
- # Returns a relation value with a given name
- def get_value(name) # :nodoc:
- @values[name] || default_value_for(name)
- end
# Sets the relation value with the given name
def set_value(name, value) # :nodoc:
@@ -921,16 +929,16 @@ module ActiveRecord
raise ImmutableRelation if defined?(@arel) && @arel
end
- def build_arel
+ def build_arel(aliases)
arel = Arel::SelectManager.new(table)
- build_joins(arel, joins_values.flatten) unless joins_values.empty?
- build_left_outer_joins(arel, left_outer_joins_values.flatten) unless left_outer_joins_values.empty?
+ aliases = build_joins(arel, joins_values.flatten, aliases) unless joins_values.empty?
+ build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases) unless left_outer_joins_values.empty?
arel.where(where_clause.ast) unless where_clause.empty?
arel.having(having_clause.ast) unless having_clause.empty?
if limit_value
- limit_attribute = Attribute.with_cast_value(
+ limit_attribute = ActiveModel::Attribute.with_cast_value(
"LIMIT".freeze,
connection.sanitize_limit(limit_value),
Type.default_value,
@@ -938,7 +946,7 @@ module ActiveRecord
arel.take(Arel::Nodes::BindParam.new(limit_attribute))
end
if offset_value
- offset_attribute = Attribute.with_cast_value(
+ offset_attribute = ActiveModel::Attribute.with_cast_value(
"OFFSET".freeze,
offset_value.to_i,
Type.default_value,
@@ -963,6 +971,9 @@ module ActiveRecord
name = from_clause.name
case opts
when Relation
+ if opts.eager_loading?
+ opts = opts.send(:apply_join_dependency)
+ end
name ||= "subquery"
opts.arel.as(name.to_s)
else
@@ -970,20 +981,22 @@ module ActiveRecord
end
end
- def build_left_outer_joins(manager, outer_joins)
+ def build_left_outer_joins(manager, outer_joins, aliases)
buckets = outer_joins.group_by do |join|
case join
when Hash, Symbol, Array
:association_join
+ when ActiveRecord::Associations::JoinDependency
+ :stashed_join
else
raise ArgumentError, "only Hash, Symbol and Array are allowed"
end
end
- build_join_query(manager, buckets, Arel::Nodes::OuterJoin)
+ build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases)
end
- def build_joins(manager, joins)
+ def build_joins(manager, joins, aliases)
buckets = joins.group_by do |join|
case join
when String
@@ -999,10 +1012,10 @@ module ActiveRecord
end
end
- build_join_query(manager, buckets, Arel::Nodes::InnerJoin)
+ build_join_query(manager, buckets, Arel::Nodes::InnerJoin, aliases)
end
- def build_join_query(manager, buckets, join_type)
+ def build_join_query(manager, buckets, join_type, aliases)
buckets.default = []
association_joins = buckets[:association_join]
@@ -1010,10 +1023,11 @@ module ActiveRecord
join_nodes = buckets[:join_node].uniq
string_joins = buckets[:string_join].map(&:strip).uniq
- join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins)
+ join_list = join_nodes + convert_join_strings_to_ast(string_joins)
+ alias_tracker = alias_tracker(join_list, aliases)
join_dependency = ActiveRecord::Associations::JoinDependency.new(
- klass, table, association_joins, join_list
+ klass, table, association_joins, alias_tracker
)
joins = join_dependency.join_constraints(stashed_association_joins, join_type)
@@ -1021,10 +1035,10 @@ module ActiveRecord
manager.join_sources.concat(join_list)
- manager
+ alias_tracker.aliases
end
- def convert_join_strings_to_ast(table, joins)
+ def convert_join_strings_to_ast(joins)
joins
.flatten
.reject(&:blank?)
@@ -1034,6 +1048,8 @@ module ActiveRecord
def build_select(arel)
if select_values.any?
arel.project(*arel_columns(select_values.uniq))
+ elsif klass.ignored_columns.any?
+ arel.project(*klass.column_names.map { |field| arel_attribute(field) })
else
arel.project(table[Arel.star])
end
@@ -1070,7 +1086,7 @@ module ActiveRecord
end
o.split(",").map! do |s|
s.strip!
- s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || s.concat(" DESC")
+ s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || (s << " DESC")
end
else
o
@@ -1079,6 +1095,10 @@ module ActiveRecord
end
def does_not_support_reverse?(order)
+ # Account for String subclasses like Arel::Nodes::SqlLiteral that
+ # override methods like #count.
+ order = String.new(order) unless order.instance_of?(String)
+
# Uses SQL function with multiple arguments.
(order.include?(",") && order.split(",").find { |section| section.count("(") != section.count(")") }) ||
# Uses "nulls first" like construction.
@@ -1109,9 +1129,15 @@ module ActiveRecord
def preprocess_order_args(order_args)
order_args.map! do |arg|
- klass.send(:sanitize_sql_for_order, arg)
+ klass.sanitize_sql_for_order(arg)
end
order_args.flatten!
+
+ @klass.enforce_raw_sql_whitelist(
+ order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a },
+ whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
+ )
+
validate_order_args(order_args)
references = order_args.grep(String)
@@ -1174,7 +1200,6 @@ module ActiveRecord
DEFAULT_VALUES = {
create_with: FROZEN_EMPTY_HASH,
- readonly: false,
where: Relation::WhereClause.empty,
having: Relation::WhereClause.empty,
from: Relation::FromClause.empty
@@ -1183,15 +1208,5 @@ module ActiveRecord
Relation::MULTI_VALUE_METHODS.each do |value|
DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY
end
-
- Relation::SINGLE_VALUE_METHODS.each do |value|
- DEFAULT_VALUES[value] = nil if DEFAULT_VALUES[value].nil?
- end
-
- def default_value_for(name)
- DEFAULT_VALUES.fetch(name) do
- raise ArgumentError, "unknown relation value #{name.inspect}"
- end
- end
end
end
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 424894f835..b092399657 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -2,7 +2,7 @@
require "active_support/core_ext/hash/except"
require "active_support/core_ext/hash/slice"
-require_relative "merger"
+require "active_record/relation/merger"
module ActiveRecord
module SpawnMethods
@@ -10,6 +10,7 @@ module ActiveRecord
def spawn #:nodoc:
clone
end
+ alias :all :spawn
# Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation.
# Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array.
@@ -69,7 +70,7 @@ module ActiveRecord
private
def relation_with(values)
- result = Relation.create(klass, table, predicate_builder, values)
+ result = Relation.create(klass, values: values)
result.extend(*extending_values) if extending_values.any?
result
end
diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb
index 752bb38481..a502713e56 100644
--- a/activerecord/lib/active_record/relation/where_clause.rb
+++ b/activerecord/lib/active_record/relation/where_clause.rb
@@ -47,7 +47,7 @@ module ActiveRecord
end
def to_h(table_name = nil)
- equalities = predicates.grep(Arel::Nodes::Equality)
+ equalities = equalities(predicates)
if table_name
equalities = equalities.select do |node|
node.left.relation.name == table_name
@@ -90,6 +90,20 @@ module ActiveRecord
end
private
+ def equalities(predicates)
+ equalities = []
+
+ predicates.each do |node|
+ case node
+ when Arel::Nodes::Equality
+ equalities << node
+ when Arel::Nodes::And
+ equalities.concat equalities(node.children)
+ end
+ end
+
+ equalities
+ end
def predicates_unreferenced_by(other)
predicates.reject do |n|
@@ -121,7 +135,7 @@ module ActiveRecord
end
def except_predicates(columns)
- self.predicates.reject do |node|
+ predicates.reject do |node|
case node
when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right)
diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
index 1374785354..c1b3eea9df 100644
--- a/activerecord/lib/active_record/relation/where_clause_factory.rb
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -11,10 +11,9 @@ module ActiveRecord
def build(opts, other)
case opts
when String, Array
- parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
+ parts = [klass.sanitize_sql(other.empty? ? opts : ([opts] + other))]
when Hash
attributes = predicate_builder.resolve_column_aliases(opts)
- attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes)
attributes.stringify_keys!
parts = predicate_builder.build_from_hash(attributes)
@@ -27,8 +26,7 @@ module ActiveRecord
WhereClause.new(parts)
end
- protected
-
+ private
attr_reader :klass, :predicate_builder
end
end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 91a4f1fad6..c6c268855e 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -5,72 +5,135 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- private
-
- # Accepts an array or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for a WHERE clause.
- #
- # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
- # # => "name='foo''bar' and group_id=4"
- #
- # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
- # # => "name='foo''bar' and group_id='4'"
- #
- # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
- # # => "name='foo''bar' and group_id='4'"
- #
- # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
- # # => "name='foo''bar' and group_id='4'"
- def sanitize_sql_for_conditions(condition) # :doc:
- return nil if condition.blank?
-
- case condition
- when Array; sanitize_sql_array(condition)
- else condition
- end
+ # Accepts an array or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for a WHERE clause.
+ #
+ # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
+ # # => "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
+ # # => "name='foo''bar' and group_id='4'"
+ def sanitize_sql_for_conditions(condition)
+ return nil if condition.blank?
+
+ case condition
+ when Array; sanitize_sql_array(condition)
+ else condition
end
- alias :sanitize_sql :sanitize_sql_for_conditions
- alias :sanitize_conditions :sanitize_sql
- deprecate sanitize_conditions: :sanitize_sql
-
- # Accepts an array, hash, or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for a SET clause.
- #
- # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
- # # => "name=NULL and group_id=4"
- #
- # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
- # # => "name=NULL and group_id=4"
- #
- # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 })
- # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
- #
- # sanitize_sql_for_assignment("name=NULL and group_id='4'")
- # # => "name=NULL and group_id='4'"
- def sanitize_sql_for_assignment(assignments, default_table_name = table_name) # :doc:
- case assignments
- when Array; sanitize_sql_array(assignments)
- when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name)
- else assignments
- end
+ end
+ alias :sanitize_sql :sanitize_sql_for_conditions
+
+ # Accepts an array, hash, or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for a SET clause.
+ #
+ # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
+ # # => "name=NULL and group_id=4"
+ #
+ # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
+ # # => "name=NULL and group_id=4"
+ #
+ # Post.sanitize_sql_for_assignment({ name: nil, group_id: 4 })
+ # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
+ #
+ # sanitize_sql_for_assignment("name=NULL and group_id='4'")
+ # # => "name=NULL and group_id='4'"
+ def sanitize_sql_for_assignment(assignments, default_table_name = table_name)
+ case assignments
+ when Array; sanitize_sql_array(assignments)
+ when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name)
+ else assignments
end
-
- # Accepts an array, or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for an ORDER clause.
- #
- # sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
- # # => "field(id, 1,3,2)"
- #
- # sanitize_sql_for_order("id ASC")
- # # => "id ASC"
- def sanitize_sql_for_order(condition) # :doc:
- if condition.is_a?(Array) && condition.first.to_s.include?("?")
- sanitize_sql_array(condition)
- else
- condition
+ end
+
+ # Accepts an array, or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for an ORDER clause.
+ #
+ # sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
+ # # => "field(id, 1,3,2)"
+ #
+ # sanitize_sql_for_order("id ASC")
+ # # => "id ASC"
+ def sanitize_sql_for_order(condition)
+ if condition.is_a?(Array) && condition.first.to_s.include?("?")
+ enforce_raw_sql_whitelist([condition.first],
+ whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
+ )
+
+ # Ensure we aren't dealing with a subclass of String that might
+ # override methods we use (eg. Arel::Nodes::SqlLiteral).
+ if condition.first.kind_of?(String) && !condition.first.instance_of?(String)
+ condition = [String.new(condition.first), *condition[1..-1]]
end
+
+ Arel.sql(sanitize_sql_array(condition))
+ else
+ condition
end
+ end
+
+ # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
+ #
+ # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
+ # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
+ def sanitize_sql_hash_for_assignment(attrs, table)
+ c = connection
+ attrs.map do |attr, value|
+ type = type_for_attribute(attr)
+ value = type.serialize(type.cast(value))
+ "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}"
+ end.join(", ")
+ end
+
+ # Sanitizes a +string+ so that it is safe to use within an SQL
+ # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
+ #
+ # sanitize_sql_like("100%")
+ # # => "100\\%"
+ #
+ # sanitize_sql_like("snake_cased_string")
+ # # => "snake\\_cased\\_string"
+ #
+ # sanitize_sql_like("100%", "!")
+ # # => "100!%"
+ #
+ # sanitize_sql_like("snake_cased_string", "!")
+ # # => "snake!_cased!_string"
+ def sanitize_sql_like(string, escape_character = "\\")
+ pattern = Regexp.union(escape_character, "%", "_")
+ string.gsub(pattern) { |x| [escape_character, x].join }
+ end
+
+ # Accepts an array of conditions. The array has each value
+ # sanitized and interpolated into the SQL statement.
+ #
+ # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
+ def sanitize_sql_array(ary)
+ statement, *values = ary
+ if values.first.is_a?(Hash) && /:\w+/.match?(statement)
+ replace_named_bind_variables(statement, values.first)
+ elsif statement.include?("?")
+ replace_bind_variables(statement, values)
+ elsif statement.blank?
+ statement
+ else
+ statement % values.collect { |value| connection.quote_string(value.to_s) }
+ end
+ end
+ private
# Accepts a hash of SQL conditions and replaces those attributes
# that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of]
# relationship with their expanded aggregate attribute values.
@@ -92,10 +155,12 @@ module ActiveRecord
if aggregation = reflect_on_aggregation(attr.to_sym)
mapping = aggregation.mapping
mapping.each do |field_attr, aggregate_attr|
- if mapping.size == 1 && !value.respond_to?(aggregate_attr)
- expanded_attrs[field_attr] = value
+ expanded_attrs[field_attr] = if value.is_a?(Array)
+ value.map { |it| it.send(aggregate_attr) }
+ elsif mapping.size == 1 && !value.respond_to?(aggregate_attr)
+ value
else
- expanded_attrs[field_attr] = value.send(aggregate_attr)
+ value.send(aggregate_attr)
end
end
else
@@ -104,61 +169,7 @@ module ActiveRecord
end
expanded_attrs
end
-
- # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
- #
- # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
- # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
- def sanitize_sql_hash_for_assignment(attrs, table) # :doc:
- c = connection
- attrs.map do |attr, value|
- value = type_for_attribute(attr.to_s).serialize(value)
- "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}"
- end.join(", ")
- end
-
- # Sanitizes a +string+ so that it is safe to use within an SQL
- # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
- #
- # sanitize_sql_like("100%")
- # # => "100\\%"
- #
- # sanitize_sql_like("snake_cased_string")
- # # => "snake\\_cased\\_string"
- #
- # sanitize_sql_like("100%", "!")
- # # => "100!%"
- #
- # sanitize_sql_like("snake_cased_string", "!")
- # # => "snake!_cased!_string"
- def sanitize_sql_like(string, escape_character = "\\") # :doc:
- pattern = Regexp.union(escape_character, "%", "_")
- string.gsub(pattern) { |x| [escape_character, x].join }
- end
-
- # Accepts an array of conditions. The array has each value
- # sanitized and interpolated into the SQL statement.
- #
- # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
- # # => "name='foo''bar' and group_id=4"
- #
- # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
- # # => "name='foo''bar' and group_id=4"
- #
- # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
- # # => "name='foo''bar' and group_id='4'"
- def sanitize_sql_array(ary) # :doc:
- statement, *values = ary
- if values.first.is_a?(Hash) && /:\w+/.match?(statement)
- replace_named_bind_variables(statement, values.first)
- elsif statement.include?("?")
- replace_bind_variables(statement, values)
- elsif statement.blank?
- statement
- else
- statement % values.collect { |value| connection.quote_string(value.to_s) }
- end
- end
+ deprecate :expand_hash_conditions_for_aggregates
def replace_bind_variables(statement, values)
raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size)
@@ -207,10 +218,5 @@ module ActiveRecord
end
end
end
-
- def quoted_id # :nodoc:
- self.class.connection.quote(@attributes[self.class.primary_key].value_for_database)
- end
- deprecate :quoted_id
end
end
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index 1e121f2a09..216359867c 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -55,7 +55,7 @@ module ActiveRecord
end
ActiveRecord::InternalMetadata.create_table
- ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
+ ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment
end
private
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 8d0311fabd..d475e77444 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -17,6 +17,12 @@ module ActiveRecord
# Only strings are accepted if ActiveRecord::Base.schema_format == :sql.
cattr_accessor :ignore_tables, default: []
+ ##
+ # :singleton-method:
+ # Specify a custom regular expression matching foreign keys which name
+ # should not be dumped to db/schema.rb.
+ cattr_accessor :fk_ignore_pattern, default: /^fk_rails_[0-9a-f]{10}$/
+
class << self
def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base)
connection.create_schema_dumper(generate_options(config)).dump(stream)
@@ -44,7 +50,7 @@ module ActiveRecord
def initialize(connection, options = {})
@connection = connection
- @version = Migrator::current_version rescue nil
+ @version = connection.migration_context.current_version rescue nil
@options = options
end
@@ -65,11 +71,11 @@ module ActiveRecord
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
-# Note that this schema.rb definition is the authoritative source for your
-# database schema. If you need to create the application database on another
-# system, you should be using db:schema:load, not running all the migrations
-# from scratch. The latter is a flawed and unsustainable approach (the more migrations
-# you'll amass, the slower it'll run and the greater likelihood for issues).
+# This file is the source Rails uses to define your schema when running `rails
+# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
@@ -82,16 +88,8 @@ HEADER
stream.puts "end"
end
+ # extensions are only supported by PostgreSQL
def extensions(stream)
- return unless @connection.supports_extensions?
- extensions = @connection.extensions
- if extensions.any?
- stream.puts " # These are extensions that must be enabled in order to support this database"
- extensions.sort.each do |extension|
- stream.puts " enable_extension #{extension.inspect}"
- end
- stream.puts
- end
end
def tables(stream)
@@ -192,8 +190,9 @@ HEADER
"name: #{index.name.inspect}",
]
index_parts << "unique: true" if index.unique
- index_parts << "length: { #{format_options(index.lengths)} }" if index.lengths.present?
- index_parts << "order: { #{format_options(index.orders)} }" if index.orders.present?
+ index_parts << "length: #{format_index_parts(index.lengths)}" if index.lengths.present?
+ index_parts << "order: #{format_index_parts(index.orders)}" if index.orders.present?
+ index_parts << "opclass: #{format_index_parts(index.opclasses)}" if index.opclasses.present?
index_parts << "where: #{index.where.inspect}" if index.where
index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index)
index_parts << "type: #{index.type.inspect}" if index.type
@@ -217,7 +216,7 @@ HEADER
parts << "primary_key: #{foreign_key.primary_key.inspect}"
end
- if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/
+ if foreign_key.export_name_on_schema_dump?
parts << "name: #{foreign_key.name.inspect}"
end
@@ -239,6 +238,14 @@ HEADER
options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ")
end
+ def format_index_parts(options)
+ if options.is_a?(Hash)
+ "{ #{format_options(options)} }"
+ else
+ options.inspect
+ end
+ end
+
def remove_prefix_and_suffix(table)
prefix = Regexp.escape(@options[:table_name_prefix].to_s)
suffix = Regexp.escape(@options[:table_name_suffix].to_s)
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 339a5334a8..f2d8b038fa 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "scoping/default"
-require_relative "scoping/named"
+require "active_record/scoping/default"
+require "active_record/scoping/named"
module ActiveRecord
# This class is used to create a table that keeps track of which migrations
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index da585a9562..01ac56570a 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -11,23 +11,23 @@ module ActiveRecord
include Named
end
- module ClassMethods
- def current_scope(skip_inherited_scope = false) # :nodoc:
+ module ClassMethods # :nodoc:
+ def current_scope(skip_inherited_scope = false)
ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope)
end
- def current_scope=(scope) #:nodoc:
+ def current_scope=(scope)
ScopeRegistry.set_value_for(:current_scope, self, scope)
end
# Collects attributes from scopes that should be applied when creating
# an AR instance for the particular class this is called on.
- def scope_attributes # :nodoc:
+ def scope_attributes
all.scope_for_create
end
# Are there attributes associated with this scope?
- def scope_attributes? # :nodoc:
+ def scope_attributes?
current_scope
end
end
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 86ae374318..8c612df27a 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -111,7 +111,7 @@ module ActiveRecord
# The user has defined their own default scope method, so call that
evaluate_default_scope do
if scope = default_scope
- (base_rel ||= relation).merge(scope)
+ (base_rel ||= relation).merge!(scope)
end
end
elsif default_scopes.any?
@@ -119,7 +119,7 @@ module ActiveRecord
evaluate_default_scope do
default_scopes.inject(base_rel) do |default_scope, scope|
scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call)
- default_scope.merge(base_rel.instance_exec(&scope))
+ default_scope.merge!(base_rel.instance_exec(&scope))
end
end
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 6fa096c1fe..a784001587 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -24,8 +24,14 @@ module ActiveRecord
# You can define a scope that applies to all finders using
# {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope].
def all
+ current_scope = self.current_scope
+
if current_scope
- current_scope.clone
+ if self == current_scope.klass
+ current_scope.clone
+ else
+ relation.merge!(current_scope)
+ end
else
default_scoped
end
@@ -165,13 +171,19 @@ module ActiveRecord
"a class method with the same name."
end
+ if method_defined_within?(name, Relation)
+ raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
+ "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \
+ "an instance method with the same name."
+ end
+
valid_scope_name?(name)
extension = Module.new(&block) if block
if body.respond_to?(:to_proc)
singleton_class.send(:define_method, name) do |*args|
scope = all
- scope = scope.instance_exec(*args, &body) || scope
+ scope = scope._exec_scope(*args, &body)
scope = scope.extending(extension) if extension
scope
end
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index 59acd63a0f..b41d3504fd 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -114,8 +114,7 @@ module ActiveRecord
end
end
- protected
-
+ private
attr_reader :query_builder, :bind_map, :klass
end
end
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index 202b82fa61..8d628359c3 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -17,8 +17,8 @@ module ActiveRecord
# You can set custom coder to encode/decode your serialized attributes to/from different formats.
# JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
#
- # NOTE: If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for
- # the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store].
+ # NOTE: If you are using structured database data types (eg. PostgreSQL +hstore+/+json+, or MySQL 5.7+
+ # +json+) there is no need for the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store].
# Simply use {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead to generate
# the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
# using a symbol.
@@ -31,10 +31,14 @@ module ActiveRecord
#
# class User < ActiveRecord::Base
# store :settings, accessors: [ :color, :homepage ], coder: JSON
+ # store :parent, accessors: [ :name ], coder: JSON, prefix: true
+ # store :spouse, accessors: [ :name ], coder: JSON, prefix: :partner
# end
#
- # u = User.new(color: 'black', homepage: '37signals.com')
+ # u = User.new(color: 'black', homepage: '37signals.com', parent_name: 'Mary', partner_name: 'Lily')
# u.color # Accessor stored attribute
+ # u.parent_name # Accessor stored attribute with prefix
+ # u.partner_name # Accessor stored attribute with custom prefix
# u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
#
# # There is no difference between strings and symbols for accessing custom attributes
@@ -44,6 +48,7 @@ module ActiveRecord
# # Add additional accessors to an existing store through store_accessor
# class SuperUser < User
# store_accessor :settings, :privileges, :servants
+ # store_accessor :parent, :birthday, prefix: true
# end
#
# The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes].
@@ -81,19 +86,29 @@ module ActiveRecord
module ClassMethods
def store(store_attribute, options = {})
serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder])
- store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
+ store_accessor(store_attribute, options[:accessors], prefix: options[:prefix]) if options.has_key? :accessors
end
- def store_accessor(store_attribute, *keys)
+ def store_accessor(store_attribute, *keys, prefix: nil)
keys = keys.flatten
+ accessor_prefix =
+ case prefix
+ when String, Symbol
+ "#{prefix}_"
+ when TrueClass
+ "#{store_attribute}_"
+ else
+ ""
+ end
+
_store_accessors_module.module_eval do
keys.each do |key|
- define_method("#{key}=") do |value|
+ define_method("#{accessor_prefix}#{key}=") do |value|
write_store_attribute(store_attribute, key, value)
end
- define_method(key) do
+ define_method("#{accessor_prefix}#{key}") do
read_store_attribute(store_attribute, key)
end
end
@@ -135,7 +150,7 @@ module ActiveRecord
end
def store_accessor_for(store_attribute)
- type_for_attribute(store_attribute.to_s).accessor
+ type_for_attribute(store_attribute).accessor
end
class HashAccessor # :nodoc:
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
index a5187efc84..b67479fb6a 100644
--- a/activerecord/lib/active_record/table_metadata.rb
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -2,8 +2,7 @@
module ActiveRecord
class TableMetadata # :nodoc:
- delegate :foreign_type, :foreign_key, :join_keys, :join_foreign_key, to: :association, prefix: true
- delegate :association_primary_key, to: :association
+ delegate :foreign_type, :foreign_key, :join_primary_key, :join_foreign_key, to: :association, prefix: true
def initialize(klass, arel_table, association = nil)
@klass = klass
@@ -31,7 +30,7 @@ module ActiveRecord
def type(column_name)
if klass
- klass.type_for_attribute(column_name.to_s)
+ klass.type_for_attribute(column_name)
else
Type.default_value
end
@@ -66,10 +65,15 @@ module ActiveRecord
association && association.polymorphic?
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ def aggregated_with?(aggregation_name)
+ klass && reflect_on_aggregation(aggregation_name)
+ end
+
+ def reflect_on_aggregation(aggregation_name)
+ klass.reflect_on_aggregation(aggregation_name)
+ end
+ private
attr_reader :klass, :arel_table, :association
end
end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 0f3f84ca08..521375954b 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -54,10 +54,10 @@ module ActiveRecord
def check_protected_environments!
unless ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"]
- current = ActiveRecord::Migrator.current_environment
- stored = ActiveRecord::Migrator.last_stored_environment
+ current = ActiveRecord::Base.connection.migration_context.current_environment
+ stored = ActiveRecord::Base.connection.migration_context.last_stored_environment
- if ActiveRecord::Migrator.protected_environment?
+ if ActiveRecord::Base.connection.migration_context.protected_environment?
raise ActiveRecord::ProtectedEnvironmentError.new(stored)
end
@@ -117,9 +117,9 @@ module ActiveRecord
def create(*arguments)
configuration = arguments.first
class_for_adapter(configuration["adapter"]).new(*arguments).create
- $stdout.puts "Created database '#{configuration['database']}'"
+ $stdout.puts "Created database '#{configuration['database']}'" if verbose?
rescue DatabaseAlreadyExists
- $stderr.puts "Database '#{configuration['database']}' already exists"
+ $stderr.puts "Database '#{configuration['database']}' already exists" if verbose?
rescue Exception => error
$stderr.puts error
$stderr.puts "Couldn't create database for #{configuration.inspect}"
@@ -134,6 +134,18 @@ module ActiveRecord
end
end
+ def for_each
+ databases = Rails.application.config.load_database_yaml
+ database_configs = ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases)
+
+ # if this is a single database application we don't want tasks for each primary database
+ return if database_configs.count == 1
+
+ database_configs.each do |db_config|
+ yield db_config.spec_name
+ end
+ end
+
def create_current(environment = env)
each_current_configuration(environment) { |configuration|
create configuration
@@ -144,7 +156,7 @@ module ActiveRecord
def drop(*arguments)
configuration = arguments.first
class_for_adapter(configuration["adapter"]).new(*arguments).drop
- $stdout.puts "Dropped database '#{configuration['database']}'"
+ $stdout.puts "Dropped database '#{configuration['database']}'" if verbose?
rescue ActiveRecord::NoDatabaseError
$stderr.puts "Database '#{configuration['database']}' does not exist"
rescue Exception => error
@@ -164,13 +176,11 @@ module ActiveRecord
end
def migrate
- raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty?
+ check_target_version
- verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true
- version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
scope = ENV["SCOPE"]
- verbose_was, Migration.verbose = Migration.verbose, verbose
- Migrator.migrate(migrations_paths, version) do |migration|
+ verbose_was, Migration.verbose = Migration.verbose, verbose?
+ Base.connection.migration_context.migrate(target_version) do |migration|
scope.blank? || scope == migration.scope
end
ActiveRecord::Base.clear_cache!
@@ -178,6 +188,16 @@ module ActiveRecord
Migration.verbose = verbose_was
end
+ def check_target_version
+ if target_version && !(Migration::MigrationFilenameRegexp.match?(ENV["VERSION"]) || /\A\d+\z/.match?(ENV["VERSION"]))
+ raise "Invalid format of target version: `VERSION=#{ENV['VERSION']}`"
+ end
+ end
+
+ def target_version
+ ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty?
+ end
+
def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
end
@@ -225,36 +245,50 @@ module ActiveRecord
class_for_adapter(configuration["adapter"]).new(*arguments).structure_load(filename, structure_load_flags)
end
- def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc:
- file ||= schema_file(format)
+ def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env, spec_name = "primary") # :nodoc:
+ file ||= dump_filename(spec_name, format)
+
+ check_schema_file(file)
+ ActiveRecord::Base.establish_connection(configuration)
case format
when :ruby
- check_schema_file(file)
- ActiveRecord::Base.establish_connection(configuration)
load(file)
when :sql
- check_schema_file(file)
structure_load(configuration, file)
else
raise ArgumentError, "unknown format #{format.inspect}"
end
ActiveRecord::InternalMetadata.create_table
- ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
+ ActiveRecord::InternalMetadata[:environment] = environment
end
def schema_file(format = ActiveRecord::Base.schema_format)
+ File.join(db_dir, schema_file_type(format))
+ end
+
+ def schema_file_type(format = ActiveRecord::Base.schema_format)
case format
when :ruby
- File.join(db_dir, "schema.rb")
+ "schema.rb"
when :sql
- File.join(db_dir, "structure.sql")
+ "structure.sql"
+ end
+ end
+
+ def dump_filename(namespace, format = ActiveRecord::Base.schema_format)
+ filename = if namespace == "primary"
+ schema_file_type(format)
+ else
+ "#{namespace}_#{schema_file_type(format)}"
end
+
+ ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename)
end
def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
- each_current_configuration(environment) { |configuration|
- load_schema configuration, format, file
+ each_current_configuration(environment) { |configuration, spec_name, env|
+ load_schema(configuration, format, file, env, spec_name)
}
ActiveRecord::Base.establish_connection(environment.to_sym)
end
@@ -288,6 +322,9 @@ module ActiveRecord
end
private
+ def verbose?
+ ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true
+ end
def class_for_adapter(adapter)
_key, task = @tasks.each_pair.detect { |pattern, _task| adapter[pattern] }
@@ -301,9 +338,10 @@ module ActiveRecord
environments = [environment]
environments << "test" if environment == "development"
- configurations = ActiveRecord::Base.configurations.values_at(*environments)
- configurations.compact.each do |configuration|
- yield configuration unless configuration["database"].blank?
+ environments.each do |env|
+ ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration|
+ yield configuration, spec_name, env
+ end
end
end
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 84265aa9e3..e697fa6def 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -3,8 +3,6 @@
module ActiveRecord
module Tasks # :nodoc:
class MySQLDatabaseTasks # :nodoc:
- ACCESS_DENIED_ERROR = 1045
-
delegate :connection, :establish_connection, to: ActiveRecord::Base
def initialize(configuration)
@@ -21,20 +19,6 @@ module ActiveRecord
else
raise
end
- rescue error_class => error
- if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR
- $stdout.print error.message
- establish_connection root_configuration_without_database
- connection.create_database configuration["database"], creation_options
- if configuration["username"] != "root"
- connection.execute grant_statement.gsub(/\s+/, " ").strip
- end
- establish_connection configuration
- else
- $stderr.puts error.inspect
- $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}"
- $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration["encoding"]
- end
end
def drop
@@ -99,37 +83,6 @@ module ActiveRecord
end
end
- def error_class
- if configuration["adapter"].include?("jdbc")
- require_relative "../railties/jdbcmysql_error"
- ArJdbcMySQL::Error
- elsif defined?(Mysql2)
- Mysql2::Error
- else
- StandardError
- end
- end
-
- def grant_statement
- <<-SQL
-GRANT ALL PRIVILEGES ON `#{configuration['database']}`.*
- TO '#{configuration['username']}'@'localhost'
-IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
- SQL
- end
-
- def root_configuration_without_database
- configuration_without_database.merge(
- "username" => "root",
- "password" => root_password
- )
- end
-
- def root_password
- $stdout.print "Please provide the root password for your MySQL installation\n>"
- $stdin.gets.strip
- end
-
def prepare_command_options
args = {
"host" => "--host",
diff --git a/activerecord/lib/active_record/test_databases.rb b/activerecord/lib/active_record/test_databases.rb
new file mode 100644
index 0000000000..606a3b0fb5
--- /dev/null
+++ b/activerecord/lib/active_record/test_databases.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "active_support/testing/parallelization"
+
+module ActiveRecord
+ module TestDatabases # :nodoc:
+ ActiveSupport::Testing::Parallelization.after_fork_hook do |i|
+ create_and_migrate(i, spec_name: Rails.env)
+ end
+
+ ActiveSupport::Testing::Parallelization.run_cleanup_hook do |i|
+ drop(i, spec_name: Rails.env)
+ end
+
+ def self.create_and_migrate(i, spec_name:)
+ old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
+
+ connection_spec = ActiveRecord::Base.configurations[spec_name]
+
+ connection_spec["database"] += "-#{i}"
+ ActiveRecord::Tasks::DatabaseTasks.create(connection_spec)
+ ActiveRecord::Base.establish_connection(connection_spec)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ ensure
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
+ ENV["VERBOSE"] = old
+ end
+
+ def self.drop(i, spec_name:)
+ old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
+ connection_spec = ActiveRecord::Base.configurations[spec_name]
+
+ ActiveRecord::Tasks::DatabaseTasks.drop(connection_spec)
+ ensure
+ ENV["VERBOSE"] = old
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 5da3759e5a..e47f06bf3a 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -52,16 +52,20 @@ module ActiveRecord
clear_timestamp_attributes
end
- class_methods do
+ module ClassMethods # :nodoc:
+ def timestamp_attributes_for_update_in_model
+ timestamp_attributes_for_update.select { |c| column_names.include?(c) }
+ end
+
+ def current_time_from_proper_timezone
+ default_timezone == :utc ? Time.now.utc : Time.now
+ end
+
private
def timestamp_attributes_for_create_in_model
timestamp_attributes_for_create.select { |c| column_names.include?(c) }
end
- def timestamp_attributes_for_update_in_model
- timestamp_attributes_for_update.select { |c| column_names.include?(c) }
- end
-
def all_timestamp_attributes_in_model
timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model
end
@@ -73,10 +77,6 @@ module ActiveRecord
def timestamp_attributes_for_update
["updated_at", "updated_on"]
end
-
- def current_time_from_proper_timezone
- default_timezone == :utc ? Time.now.utc : Time.now
- end
end
private
@@ -116,7 +116,7 @@ module ActiveRecord
end
def timestamp_attributes_for_update_in_model
- self.class.send(:timestamp_attributes_for_update_in_model)
+ self.class.timestamp_attributes_for_update_in_model
end
def all_timestamp_attributes_in_model
@@ -124,7 +124,7 @@ module ActiveRecord
end
def current_time_from_proper_timezone
- self.class.send(:current_time_from_proper_timezone)
+ self.class.current_time_from_proper_timezone
end
def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update_in_model)
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 6761f2da25..97cba5d1c7 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -285,7 +285,7 @@ module ActiveRecord
fire_on = Array(options[:on])
assert_valid_transaction_action(fire_on)
options[:if] = Array(options[:if])
- options[:if].unshift("transaction_include_any_action?(#{fire_on})")
+ options[:if].unshift(-> { transaction_include_any_action?(fire_on) })
end
end
diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb
index 3cf70eafb8..82661a328a 100644
--- a/activerecord/lib/active_record/translation.rb
+++ b/activerecord/lib/active_record/translation.rb
@@ -10,7 +10,7 @@ module ActiveRecord
classes = [klass]
return classes if klass == ActiveRecord::Base
- while klass != klass.base_class
+ while !klass.base_class?
classes << klass = klass.superclass
end
classes
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index fa22df92b8..c303186ef2 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -2,21 +2,21 @@
require "active_model/type"
-require_relative "type/internal/timezone"
+require "active_record/type/internal/timezone"
-require_relative "type/date"
-require_relative "type/date_time"
-require_relative "type/decimal_without_scale"
-require_relative "type/json"
-require_relative "type/time"
-require_relative "type/text"
-require_relative "type/unsigned_integer"
+require "active_record/type/date"
+require "active_record/type/date_time"
+require "active_record/type/decimal_without_scale"
+require "active_record/type/json"
+require "active_record/type/time"
+require "active_record/type/text"
+require "active_record/type/unsigned_integer"
-require_relative "type/serialized"
-require_relative "type/adapter_specific_registry"
+require "active_record/type/serialized"
+require "active_record/type/adapter_specific_registry"
-require_relative "type/type_map"
-require_relative "type/hash_lookup_type_map"
+require "active_record/type/type_map"
+require "active_record/type/hash_lookup_type_map"
module ActiveRecord
module Type
diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb
index e7468aa542..b300fdfa05 100644
--- a/activerecord/lib/active_record/type/adapter_specific_registry.rb
+++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb
@@ -52,8 +52,6 @@ module ActiveRecord
priority <=> other.priority
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
protected
attr_reader :name, :block, :adapter, :override
@@ -114,13 +112,8 @@ module ActiveRecord
super | 4
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :options, :klass
-
private
+ attr_reader :options, :klass
def matches_options?(**kwargs)
options.all? do |key, value|
diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb
index ed2e4fb79c..2e5f45fa3d 100644
--- a/activerecord/lib/active_record/type_caster.rb
+++ b/activerecord/lib/active_record/type_caster.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "type_caster/map"
-require_relative "type_caster/connection"
+require "active_record/type_caster/map"
+require "active_record/type_caster/connection"
module ActiveRecord
module TypeCaster # :nodoc:
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
index af4e4e37e2..7cf8181d8e 100644
--- a/activerecord/lib/active_record/type_caster/connection.rb
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -14,15 +14,10 @@ module ActiveRecord
connection.type_cast_from_column(column, value)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :table_name
delegate :connection, to: :@klass
- private
-
def column_for(attribute_name)
if connection.schema_cache.data_source_exists?(table_name)
connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb
index d51350ba83..663cdadb03 100644
--- a/activerecord/lib/active_record/type_caster/map.rb
+++ b/activerecord/lib/active_record/type_caster/map.rb
@@ -9,14 +9,11 @@ module ActiveRecord
def type_cast_for_database(attr_name, value)
return value if value.is_a?(Arel::Nodes::BindParam)
- type = types.type_for_attribute(attr_name.to_s)
+ type = types.type_for_attribute(attr_name)
type.serialize(value)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :types
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 3f5c879f2f..ca27a3f0ab 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -86,8 +86,8 @@ module ActiveRecord
end
end
-require_relative "validations/associated"
-require_relative "validations/uniqueness"
-require_relative "validations/presence"
-require_relative "validations/absence"
-require_relative "validations/length"
+require "active_record/validations/associated"
+require "active_record/validations/uniqueness"
+require "active_record/validations/presence"
+require "active_record/validations/absence"
+require "active_record/validations/length"
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index f4ad58c087..5a1dbc8e53 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -23,7 +23,7 @@ module ActiveRecord
relation = build_relation(finder_class, attribute, value)
if record.persisted?
if finder_class.primary_key
- relation = relation.where.not(finder_class.primary_key => record.id_in_database || record.id)
+ relation = relation.where.not(finder_class.primary_key => record.id_in_database)
else
raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
end
@@ -205,9 +205,7 @@ module ActiveRecord
# | # Boom! We now have a duplicate
# | # title!
#
- # This could even happen if you use transactions with the 'serializable'
- # isolation level. The best way to work around this problem is to add a unique
- # index to the database table using
+ # The best way to work around this problem is to add a unique index to the database table using
# {connection.add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index].
# In the rare case that a race condition occurs, the database will guarantee
# the field's uniqueness.
diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb
new file mode 100644
index 0000000000..7d04e1cac6
--- /dev/null
+++ b/activerecord/lib/arel.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "arel/errors"
+
+require "arel/crud"
+require "arel/factory_methods"
+
+require "arel/expressions"
+require "arel/predications"
+require "arel/window_predications"
+require "arel/math"
+require "arel/alias_predication"
+require "arel/order_predications"
+require "arel/table"
+require "arel/attributes"
+require "arel/compatibility/wheres"
+
+require "arel/visitors"
+require "arel/collectors/sql_string"
+
+require "arel/tree_manager"
+require "arel/insert_manager"
+require "arel/select_manager"
+require "arel/update_manager"
+require "arel/delete_manager"
+require "arel/nodes"
+
+module Arel # :nodoc: all
+ VERSION = "10.0.0"
+
+ def self.sql(raw_sql)
+ Arel::Nodes::SqlLiteral.new raw_sql
+ end
+
+ def self.star
+ sql "*"
+ end
+ ## Convenience Alias
+ Node = Arel::Nodes::Node
+end
diff --git a/activerecord/lib/arel/alias_predication.rb b/activerecord/lib/arel/alias_predication.rb
new file mode 100644
index 0000000000..4abbbb7ef6
--- /dev/null
+++ b/activerecord/lib/arel/alias_predication.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module AliasPredication
+ def as(other)
+ Nodes::As.new self, Nodes::SqlLiteral.new(other)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/attributes.rb b/activerecord/lib/arel/attributes.rb
new file mode 100644
index 0000000000..35d586c948
--- /dev/null
+++ b/activerecord/lib/arel/attributes.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "arel/attributes/attribute"
+
+module Arel # :nodoc: all
+ module Attributes
+ ###
+ # Factory method to wrap a raw database +column+ to an Arel Attribute.
+ def self.for(column)
+ case column.type
+ when :string, :text, :binary then String
+ when :integer then Integer
+ when :float then Float
+ when :decimal then Decimal
+ when :date, :datetime, :timestamp, :time then Time
+ when :boolean then Boolean
+ else
+ Undefined
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/attributes/attribute.rb b/activerecord/lib/arel/attributes/attribute.rb
new file mode 100644
index 0000000000..ecf499a23e
--- /dev/null
+++ b/activerecord/lib/arel/attributes/attribute.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Attributes
+ class Attribute < Struct.new :relation, :name
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::AliasPredication
+ include Arel::OrderPredications
+ include Arel::Math
+
+ ###
+ # Create a node for lowering this attribute
+ def lower
+ relation.lower self
+ end
+
+ def type_cast_for_database(value)
+ relation.type_cast_for_database(name, value)
+ end
+
+ def able_to_type_cast?
+ relation.able_to_type_cast?
+ end
+ end
+
+ class String < Attribute; end
+ class Time < Attribute; end
+ class Boolean < Attribute; end
+ class Decimal < Attribute; end
+ class Float < Attribute; end
+ class Integer < Attribute; end
+ class Undefined < Attribute; end
+ end
+
+ Attribute = Attributes::Attribute
+end
diff --git a/activerecord/lib/arel/collectors/bind.rb b/activerecord/lib/arel/collectors/bind.rb
new file mode 100644
index 0000000000..6f8912575d
--- /dev/null
+++ b/activerecord/lib/arel/collectors/bind.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class Bind
+ def initialize
+ @binds = []
+ end
+
+ def <<(str)
+ self
+ end
+
+ def add_bind(bind)
+ @binds << bind
+ self
+ end
+
+ def value
+ @binds
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/composite.rb b/activerecord/lib/arel/collectors/composite.rb
new file mode 100644
index 0000000000..d040d8598d
--- /dev/null
+++ b/activerecord/lib/arel/collectors/composite.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class Composite
+ def initialize(left, right)
+ @left = left
+ @right = right
+ end
+
+ def <<(str)
+ left << str
+ right << str
+ self
+ end
+
+ def add_bind(bind, &block)
+ left.add_bind bind, &block
+ right.add_bind bind, &block
+ self
+ end
+
+ def value
+ [left.value, right.value]
+ end
+
+ protected
+
+ attr_reader :left, :right
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/plain_string.rb b/activerecord/lib/arel/collectors/plain_string.rb
new file mode 100644
index 0000000000..687d7fbf2f
--- /dev/null
+++ b/activerecord/lib/arel/collectors/plain_string.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class PlainString
+ def initialize
+ @str = "".dup
+ end
+
+ def value
+ @str
+ end
+
+ def <<(str)
+ @str << str
+ self
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/sql_string.rb b/activerecord/lib/arel/collectors/sql_string.rb
new file mode 100644
index 0000000000..c293a89a74
--- /dev/null
+++ b/activerecord/lib/arel/collectors/sql_string.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "arel/collectors/plain_string"
+
+module Arel # :nodoc: all
+ module Collectors
+ class SQLString < PlainString
+ def initialize(*)
+ super
+ @bind_index = 1
+ end
+
+ def add_bind(bind)
+ self << yield(@bind_index)
+ @bind_index += 1
+ self
+ end
+
+ def compile(bvs)
+ value
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/substitute_binds.rb b/activerecord/lib/arel/collectors/substitute_binds.rb
new file mode 100644
index 0000000000..3f40eec8a8
--- /dev/null
+++ b/activerecord/lib/arel/collectors/substitute_binds.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class SubstituteBinds
+ def initialize(quoter, delegate_collector)
+ @quoter = quoter
+ @delegate = delegate_collector
+ end
+
+ def <<(str)
+ delegate << str
+ self
+ end
+
+ def add_bind(bind)
+ self << quoter.quote(bind)
+ end
+
+ def value
+ delegate.value
+ end
+
+ protected
+
+ attr_reader :quoter, :delegate
+ end
+ end
+end
diff --git a/activerecord/lib/arel/compatibility/wheres.rb b/activerecord/lib/arel/compatibility/wheres.rb
new file mode 100644
index 0000000000..c8a73f0dae
--- /dev/null
+++ b/activerecord/lib/arel/compatibility/wheres.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Compatibility # :nodoc:
+ class Wheres # :nodoc:
+ include Enumerable
+
+ module Value # :nodoc:
+ attr_accessor :visitor
+ def value
+ visitor.accept self
+ end
+
+ def name
+ super.to_sym
+ end
+ end
+
+ def initialize(engine, collection)
+ @engine = engine
+ @collection = collection
+ end
+
+ def each
+ to_sql = Visitors::ToSql.new @engine
+
+ @collection.each { |c|
+ c.extend(Value)
+ c.visitor = to_sql
+ yield c
+ }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/crud.rb b/activerecord/lib/arel/crud.rb
new file mode 100644
index 0000000000..e8a563ca4a
--- /dev/null
+++ b/activerecord/lib/arel/crud.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ ###
+ # FIXME hopefully we can remove this
+ module Crud
+ def compile_update(values, pk)
+ um = UpdateManager.new
+
+ if Nodes::SqlLiteral === values
+ relation = @ctx.from
+ else
+ relation = values.first.first.relation
+ end
+ um.key = pk
+ um.table relation
+ um.set values
+ um.take @ast.limit.expr if @ast.limit
+ um.order(*@ast.orders)
+ um.wheres = @ctx.wheres
+ um
+ end
+
+ def compile_insert(values)
+ im = create_insert
+ im.insert values
+ im
+ end
+
+ def create_insert
+ InsertManager.new
+ end
+
+ def compile_delete
+ dm = DeleteManager.new
+ dm.take @ast.limit.expr if @ast.limit
+ dm.wheres = @ctx.wheres
+ dm.from @ctx.froms
+ dm
+ end
+ end
+end
diff --git a/activerecord/lib/arel/delete_manager.rb b/activerecord/lib/arel/delete_manager.rb
new file mode 100644
index 0000000000..2def581009
--- /dev/null
+++ b/activerecord/lib/arel/delete_manager.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class DeleteManager < Arel::TreeManager
+ def initialize
+ super
+ @ast = Nodes::DeleteStatement.new
+ @ctx = @ast
+ end
+
+ def from(relation)
+ @ast.relation = relation
+ self
+ end
+
+ def take(limit)
+ @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit
+ self
+ end
+
+ def wheres=(list)
+ @ast.wheres = list
+ end
+ end
+end
diff --git a/activerecord/lib/arel/errors.rb b/activerecord/lib/arel/errors.rb
new file mode 100644
index 0000000000..2f8d5e3c02
--- /dev/null
+++ b/activerecord/lib/arel/errors.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class ArelError < StandardError
+ end
+
+ class EmptyJoinError < ArelError
+ end
+end
diff --git a/activerecord/lib/arel/expressions.rb b/activerecord/lib/arel/expressions.rb
new file mode 100644
index 0000000000..da8afb338c
--- /dev/null
+++ b/activerecord/lib/arel/expressions.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Expressions
+ def count(distinct = false)
+ Nodes::Count.new [self], distinct
+ end
+
+ def sum
+ Nodes::Sum.new [self]
+ end
+
+ def maximum
+ Nodes::Max.new [self]
+ end
+
+ def minimum
+ Nodes::Min.new [self]
+ end
+
+ def average
+ Nodes::Avg.new [self]
+ end
+
+ def extract(field)
+ Nodes::Extract.new [self], field
+ end
+ end
+end
diff --git a/activerecord/lib/arel/factory_methods.rb b/activerecord/lib/arel/factory_methods.rb
new file mode 100644
index 0000000000..b828bc274e
--- /dev/null
+++ b/activerecord/lib/arel/factory_methods.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ ###
+ # Methods for creating various nodes
+ module FactoryMethods
+ def create_true
+ Arel::Nodes::True.new
+ end
+
+ def create_false
+ Arel::Nodes::False.new
+ end
+
+ def create_table_alias(relation, name)
+ Nodes::TableAlias.new(relation, name)
+ end
+
+ def create_join(to, constraint = nil, klass = Nodes::InnerJoin)
+ klass.new(to, constraint)
+ end
+
+ def create_string_join(to)
+ create_join to, nil, Nodes::StringJoin
+ end
+
+ def create_and(clauses)
+ Nodes::And.new clauses
+ end
+
+ def create_on(expr)
+ Nodes::On.new expr
+ end
+
+ def grouping(expr)
+ Nodes::Grouping.new expr
+ end
+
+ ###
+ # Create a LOWER() function
+ def lower(column)
+ Nodes::NamedFunction.new "LOWER", [Nodes.build_quoted(column)]
+ end
+ end
+end
diff --git a/activerecord/lib/arel/insert_manager.rb b/activerecord/lib/arel/insert_manager.rb
new file mode 100644
index 0000000000..c90fc33a48
--- /dev/null
+++ b/activerecord/lib/arel/insert_manager.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class InsertManager < Arel::TreeManager
+ def initialize
+ super
+ @ast = Nodes::InsertStatement.new
+ end
+
+ def into(table)
+ @ast.relation = table
+ self
+ end
+
+ def columns; @ast.columns end
+ def values=(val); @ast.values = val; end
+
+ def select(select)
+ @ast.select = select
+ end
+
+ def insert(fields)
+ return if fields.empty?
+
+ if String === fields
+ @ast.values = Nodes::SqlLiteral.new(fields)
+ else
+ @ast.relation ||= fields.first.first.relation
+
+ values = []
+
+ fields.each do |column, value|
+ @ast.columns << column
+ values << value
+ end
+ @ast.values = create_values values, @ast.columns
+ end
+ self
+ end
+
+ def create_values(values, columns)
+ Nodes::Values.new values, columns
+ end
+
+ def create_values_list(rows)
+ Nodes::ValuesList.new(rows)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/math.rb b/activerecord/lib/arel/math.rb
new file mode 100644
index 0000000000..2359f13148
--- /dev/null
+++ b/activerecord/lib/arel/math.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Math
+ def *(other)
+ Arel::Nodes::Multiplication.new(self, other)
+ end
+
+ def +(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::Addition.new(self, other))
+ end
+
+ def -(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::Subtraction.new(self, other))
+ end
+
+ def /(other)
+ Arel::Nodes::Division.new(self, other)
+ end
+
+ def &(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(self, other))
+ end
+
+ def |(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(self, other))
+ end
+
+ def ^(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseXor.new(self, other))
+ end
+
+ def <<(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseShiftLeft.new(self, other))
+ end
+
+ def >>(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseShiftRight.new(self, other))
+ end
+
+ def ~@
+ Arel::Nodes::BitwiseNot.new(self)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes.rb b/activerecord/lib/arel/nodes.rb
new file mode 100644
index 0000000000..5af0e532e2
--- /dev/null
+++ b/activerecord/lib/arel/nodes.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+# node
+require "arel/nodes/node"
+require "arel/nodes/node_expression"
+require "arel/nodes/select_statement"
+require "arel/nodes/select_core"
+require "arel/nodes/insert_statement"
+require "arel/nodes/update_statement"
+require "arel/nodes/bind_param"
+
+# terminal
+
+require "arel/nodes/terminal"
+require "arel/nodes/true"
+require "arel/nodes/false"
+
+# unary
+require "arel/nodes/unary"
+require "arel/nodes/grouping"
+require "arel/nodes/ascending"
+require "arel/nodes/descending"
+require "arel/nodes/unqualified_column"
+require "arel/nodes/with"
+
+# binary
+require "arel/nodes/binary"
+require "arel/nodes/equality"
+require "arel/nodes/in" # Why is this subclassed from equality?
+require "arel/nodes/join_source"
+require "arel/nodes/delete_statement"
+require "arel/nodes/table_alias"
+require "arel/nodes/infix_operation"
+require "arel/nodes/unary_operation"
+require "arel/nodes/over"
+require "arel/nodes/matches"
+require "arel/nodes/regexp"
+
+# nary
+require "arel/nodes/and"
+
+# function
+# FIXME: Function + Alias can be rewritten as a Function and Alias node.
+# We should make Function a Unary node and deprecate the use of "aliaz"
+require "arel/nodes/function"
+require "arel/nodes/count"
+require "arel/nodes/extract"
+require "arel/nodes/values"
+require "arel/nodes/values_list"
+require "arel/nodes/named_function"
+
+# windows
+require "arel/nodes/window"
+
+# conditional expressions
+require "arel/nodes/case"
+
+# joins
+require "arel/nodes/full_outer_join"
+require "arel/nodes/inner_join"
+require "arel/nodes/outer_join"
+require "arel/nodes/right_outer_join"
+require "arel/nodes/string_join"
+
+require "arel/nodes/sql_literal"
+
+require "arel/nodes/casted"
diff --git a/activerecord/lib/arel/nodes/and.rb b/activerecord/lib/arel/nodes/and.rb
new file mode 100644
index 0000000000..c530a77bfb
--- /dev/null
+++ b/activerecord/lib/arel/nodes/and.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class And < Arel::Nodes::Node
+ attr_reader :children
+
+ def initialize(children)
+ super()
+ @children = children
+ end
+
+ def left
+ children.first
+ end
+
+ def right
+ children[1]
+ end
+
+ def hash
+ children.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.children == other.children
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/ascending.rb b/activerecord/lib/arel/nodes/ascending.rb
new file mode 100644
index 0000000000..8b617f4df5
--- /dev/null
+++ b/activerecord/lib/arel/nodes/ascending.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Ascending < Ordering
+ def reverse
+ Descending.new(expr)
+ end
+
+ def direction
+ :asc
+ end
+
+ def ascending?
+ true
+ end
+
+ def descending?
+ false
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/binary.rb b/activerecord/lib/arel/nodes/binary.rb
new file mode 100644
index 0000000000..e184e99c73
--- /dev/null
+++ b/activerecord/lib/arel/nodes/binary.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Binary < Arel::Nodes::NodeExpression
+ attr_accessor :left, :right
+
+ def initialize(left, right)
+ super()
+ @left = left
+ @right = right
+ end
+
+ def initialize_copy(other)
+ super
+ @left = @left.clone if @left
+ @right = @right.clone if @right
+ end
+
+ def hash
+ [self.class, @left, @right].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.left == other.left &&
+ self.right == other.right
+ end
+ alias :== :eql?
+ end
+
+ %w{
+ As
+ Assignment
+ Between
+ GreaterThan
+ GreaterThanOrEqual
+ Join
+ LessThan
+ LessThanOrEqual
+ NotEqual
+ NotIn
+ Or
+ Union
+ UnionAll
+ Intersect
+ Except
+ }.each do |name|
+ const_set name, Class.new(Binary)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/bind_param.rb b/activerecord/lib/arel/nodes/bind_param.rb
new file mode 100644
index 0000000000..53c5563d93
--- /dev/null
+++ b/activerecord/lib/arel/nodes/bind_param.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class BindParam < Node
+ attr_accessor :value
+
+ def initialize(value)
+ @value = value
+ super()
+ end
+
+ def hash
+ [self.class, self.value].hash
+ end
+
+ def eql?(other)
+ other.is_a?(BindParam) &&
+ value == other.value
+ end
+ alias :== :eql?
+
+ def nil?
+ value.nil?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/case.rb b/activerecord/lib/arel/nodes/case.rb
new file mode 100644
index 0000000000..654a54825e
--- /dev/null
+++ b/activerecord/lib/arel/nodes/case.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Case < Arel::Nodes::Node
+ attr_accessor :case, :conditions, :default
+
+ def initialize(expression = nil, default = nil)
+ @case = expression
+ @conditions = []
+ @default = default
+ end
+
+ def when(condition, expression = nil)
+ @conditions << When.new(Nodes.build_quoted(condition), expression)
+ self
+ end
+
+ def then(expression)
+ @conditions.last.right = Nodes.build_quoted(expression)
+ self
+ end
+
+ def else(expression)
+ @default = Else.new Nodes.build_quoted(expression)
+ self
+ end
+
+ def initialize_copy(other)
+ super
+ @case = @case.clone if @case
+ @conditions = @conditions.map { |x| x.clone }
+ @default = @default.clone if @default
+ end
+
+ def hash
+ [@case, @conditions, @default].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.case == other.case &&
+ self.conditions == other.conditions &&
+ self.default == other.default
+ end
+ alias :== :eql?
+ end
+
+ class When < Binary # :nodoc:
+ end
+
+ class Else < Unary # :nodoc:
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/casted.rb b/activerecord/lib/arel/nodes/casted.rb
new file mode 100644
index 0000000000..c1e6e97d6d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/casted.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Casted < Arel::Nodes::NodeExpression # :nodoc:
+ attr_reader :val, :attribute
+ def initialize(val, attribute)
+ @val = val
+ @attribute = attribute
+ super()
+ end
+
+ def nil?; @val.nil?; end
+
+ def hash
+ [self.class, val, attribute].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.val == other.val &&
+ self.attribute == other.attribute
+ end
+ alias :== :eql?
+ end
+
+ class Quoted < Arel::Nodes::Unary # :nodoc:
+ alias :val :value
+ def nil?; val.nil?; end
+ end
+
+ def self.build_quoted(other, attribute = nil)
+ case other
+ when Arel::Nodes::Node, Arel::Attributes::Attribute, Arel::Table, Arel::Nodes::BindParam, Arel::SelectManager, Arel::Nodes::Quoted, Arel::Nodes::SqlLiteral
+ other
+ else
+ case attribute
+ when Arel::Attributes::Attribute
+ Casted.new other, attribute
+ else
+ Quoted.new other
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/count.rb b/activerecord/lib/arel/nodes/count.rb
new file mode 100644
index 0000000000..880464639d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/count.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Count < Arel::Nodes::Function
+ def initialize(expr, distinct = false, aliaz = nil)
+ super(expr, aliaz)
+ @distinct = distinct
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/delete_statement.rb b/activerecord/lib/arel/nodes/delete_statement.rb
new file mode 100644
index 0000000000..eaac05e2f6
--- /dev/null
+++ b/activerecord/lib/arel/nodes/delete_statement.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class DeleteStatement < Arel::Nodes::Node
+ attr_accessor :left, :right
+ attr_accessor :limit
+
+ alias :relation :left
+ alias :relation= :left=
+ alias :wheres :right
+ alias :wheres= :right=
+
+ def initialize(relation = nil, wheres = [])
+ super()
+ @left = relation
+ @right = wheres
+ end
+
+ def initialize_copy(other)
+ super
+ @left = @left.clone if @left
+ @right = @right.clone if @right
+ end
+
+ def hash
+ [self.class, @left, @right].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.left == other.left &&
+ self.right == other.right
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/descending.rb b/activerecord/lib/arel/nodes/descending.rb
new file mode 100644
index 0000000000..f3f6992ca8
--- /dev/null
+++ b/activerecord/lib/arel/nodes/descending.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Descending < Ordering
+ def reverse
+ Ascending.new(expr)
+ end
+
+ def direction
+ :desc
+ end
+
+ def ascending?
+ false
+ end
+
+ def descending?
+ true
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/equality.rb b/activerecord/lib/arel/nodes/equality.rb
new file mode 100644
index 0000000000..2aa85a977e
--- /dev/null
+++ b/activerecord/lib/arel/nodes/equality.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Equality < Arel::Nodes::Binary
+ def operator; :== end
+ alias :operand1 :left
+ alias :operand2 :right
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/extract.rb b/activerecord/lib/arel/nodes/extract.rb
new file mode 100644
index 0000000000..5799ee9b8f
--- /dev/null
+++ b/activerecord/lib/arel/nodes/extract.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Extract < Arel::Nodes::Unary
+ attr_accessor :field
+
+ def initialize(expr, field)
+ super(expr)
+ @field = field
+ end
+
+ def hash
+ super ^ @field.hash
+ end
+
+ def eql?(other)
+ super &&
+ self.field == other.field
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/false.rb b/activerecord/lib/arel/nodes/false.rb
new file mode 100644
index 0000000000..1e5bf04be5
--- /dev/null
+++ b/activerecord/lib/arel/nodes/false.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class False < Arel::Nodes::NodeExpression
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/full_outer_join.rb b/activerecord/lib/arel/nodes/full_outer_join.rb
new file mode 100644
index 0000000000..91bb81f2e3
--- /dev/null
+++ b/activerecord/lib/arel/nodes/full_outer_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class FullOuterJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/function.rb b/activerecord/lib/arel/nodes/function.rb
new file mode 100644
index 0000000000..0a439b39f5
--- /dev/null
+++ b/activerecord/lib/arel/nodes/function.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Function < Arel::Nodes::NodeExpression
+ include Arel::WindowPredications
+ attr_accessor :expressions, :alias, :distinct
+
+ def initialize(expr, aliaz = nil)
+ super()
+ @expressions = expr
+ @alias = aliaz && SqlLiteral.new(aliaz)
+ @distinct = false
+ end
+
+ def as(aliaz)
+ self.alias = SqlLiteral.new(aliaz)
+ self
+ end
+
+ def hash
+ [@expressions, @alias, @distinct].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.expressions == other.expressions &&
+ self.alias == other.alias &&
+ self.distinct == other.distinct
+ end
+ alias :== :eql?
+ end
+
+ %w{
+ Sum
+ Exists
+ Max
+ Min
+ Avg
+ }.each do |name|
+ const_set(name, Class.new(Function))
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/grouping.rb b/activerecord/lib/arel/nodes/grouping.rb
new file mode 100644
index 0000000000..4d0bd69d4d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/grouping.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Grouping < Unary
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/in.rb b/activerecord/lib/arel/nodes/in.rb
new file mode 100644
index 0000000000..2be45d6f99
--- /dev/null
+++ b/activerecord/lib/arel/nodes/in.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class In < Equality
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/infix_operation.rb b/activerecord/lib/arel/nodes/infix_operation.rb
new file mode 100644
index 0000000000..bc7e20dcc6
--- /dev/null
+++ b/activerecord/lib/arel/nodes/infix_operation.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class InfixOperation < Binary
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::OrderPredications
+ include Arel::AliasPredication
+ include Arel::Math
+
+ attr_reader :operator
+
+ def initialize(operator, left, right)
+ super(left, right)
+ @operator = operator
+ end
+ end
+
+ class Multiplication < InfixOperation
+ def initialize(left, right)
+ super(:*, left, right)
+ end
+ end
+
+ class Division < InfixOperation
+ def initialize(left, right)
+ super(:/, left, right)
+ end
+ end
+
+ class Addition < InfixOperation
+ def initialize(left, right)
+ super(:+, left, right)
+ end
+ end
+
+ class Subtraction < InfixOperation
+ def initialize(left, right)
+ super(:-, left, right)
+ end
+ end
+
+ class Concat < InfixOperation
+ def initialize(left, right)
+ super("||", left, right)
+ end
+ end
+
+ class BitwiseAnd < InfixOperation
+ def initialize(left, right)
+ super(:&, left, right)
+ end
+ end
+
+ class BitwiseOr < InfixOperation
+ def initialize(left, right)
+ super(:|, left, right)
+ end
+ end
+
+ class BitwiseXor < InfixOperation
+ def initialize(left, right)
+ super(:^, left, right)
+ end
+ end
+
+ class BitwiseShiftLeft < InfixOperation
+ def initialize(left, right)
+ super(:<<, left, right)
+ end
+ end
+
+ class BitwiseShiftRight < InfixOperation
+ def initialize(left, right)
+ super(:>>, left, right)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/inner_join.rb b/activerecord/lib/arel/nodes/inner_join.rb
new file mode 100644
index 0000000000..519fafad09
--- /dev/null
+++ b/activerecord/lib/arel/nodes/inner_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class InnerJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/insert_statement.rb b/activerecord/lib/arel/nodes/insert_statement.rb
new file mode 100644
index 0000000000..d28fd1f6c8
--- /dev/null
+++ b/activerecord/lib/arel/nodes/insert_statement.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class InsertStatement < Arel::Nodes::Node
+ attr_accessor :relation, :columns, :values, :select
+
+ def initialize
+ super()
+ @relation = nil
+ @columns = []
+ @values = nil
+ @select = nil
+ end
+
+ def initialize_copy(other)
+ super
+ @columns = @columns.clone
+ @values = @values.clone if @values
+ @select = @select.clone if @select
+ end
+
+ def hash
+ [@relation, @columns, @values, @select].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.relation == other.relation &&
+ self.columns == other.columns &&
+ self.select == other.select &&
+ self.values == other.values
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/join_source.rb b/activerecord/lib/arel/nodes/join_source.rb
new file mode 100644
index 0000000000..abf0944623
--- /dev/null
+++ b/activerecord/lib/arel/nodes/join_source.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ ###
+ # Class that represents a join source
+ #
+ # http://www.sqlite.org/syntaxdiagrams.html#join-source
+
+ class JoinSource < Arel::Nodes::Binary
+ def initialize(single_source, joinop = [])
+ super
+ end
+
+ def empty?
+ !left && right.empty?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/matches.rb b/activerecord/lib/arel/nodes/matches.rb
new file mode 100644
index 0000000000..fd5734f4bd
--- /dev/null
+++ b/activerecord/lib/arel/nodes/matches.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Matches < Binary
+ attr_reader :escape
+ attr_accessor :case_sensitive
+
+ def initialize(left, right, escape = nil, case_sensitive = false)
+ super(left, right)
+ @escape = escape && Nodes.build_quoted(escape)
+ @case_sensitive = case_sensitive
+ end
+ end
+
+ class DoesNotMatch < Matches; end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/named_function.rb b/activerecord/lib/arel/nodes/named_function.rb
new file mode 100644
index 0000000000..126462d6d6
--- /dev/null
+++ b/activerecord/lib/arel/nodes/named_function.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class NamedFunction < Arel::Nodes::Function
+ attr_accessor :name
+
+ def initialize(name, expr, aliaz = nil)
+ super(expr, aliaz)
+ @name = name
+ end
+
+ def hash
+ super ^ @name.hash
+ end
+
+ def eql?(other)
+ super && self.name == other.name
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/node.rb b/activerecord/lib/arel/nodes/node.rb
new file mode 100644
index 0000000000..2b9b1e9828
--- /dev/null
+++ b/activerecord/lib/arel/nodes/node.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ ###
+ # Abstract base class for all AST nodes
+ class Node
+ include Arel::FactoryMethods
+ include Enumerable
+
+ if $DEBUG
+ def _caller
+ @caller
+ end
+
+ def initialize
+ @caller = caller.dup
+ end
+ end
+
+ ###
+ # Factory method to create a Nodes::Not node that has the recipient of
+ # the caller as a child.
+ def not
+ Nodes::Not.new self
+ end
+
+ ###
+ # Factory method to create a Nodes::Grouping node that has an Nodes::Or
+ # node as a child.
+ def or(right)
+ Nodes::Grouping.new Nodes::Or.new(self, right)
+ end
+
+ ###
+ # Factory method to create an Nodes::And node.
+ def and(right)
+ Nodes::And.new [self, right]
+ end
+
+ # FIXME: this method should go away. I don't like people calling
+ # to_sql on non-head nodes. This forces us to walk the AST until we
+ # can find a node that has a "relation" member.
+ #
+ # Maybe we should just use `Table.engine`? :'(
+ def to_sql(engine = Table.engine)
+ collector = Arel::Collectors::SQLString.new
+ collector = engine.connection.visitor.accept self, collector
+ collector.value
+ end
+
+ # Iterate through AST, nodes will be yielded depth-first
+ def each(&block)
+ return enum_for(:each) unless block_given?
+
+ ::Arel::Visitors::DepthFirst.new(block).accept self
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/node_expression.rb b/activerecord/lib/arel/nodes/node_expression.rb
new file mode 100644
index 0000000000..cbcfaba37c
--- /dev/null
+++ b/activerecord/lib/arel/nodes/node_expression.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class NodeExpression < Arel::Nodes::Node
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::AliasPredication
+ include Arel::OrderPredications
+ include Arel::Math
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/outer_join.rb b/activerecord/lib/arel/nodes/outer_join.rb
new file mode 100644
index 0000000000..0a3042be61
--- /dev/null
+++ b/activerecord/lib/arel/nodes/outer_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class OuterJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/over.rb b/activerecord/lib/arel/nodes/over.rb
new file mode 100644
index 0000000000..91176764a9
--- /dev/null
+++ b/activerecord/lib/arel/nodes/over.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Over < Binary
+ include Arel::AliasPredication
+
+ def initialize(left, right = nil)
+ super(left, right)
+ end
+
+ def operator; "OVER" end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/regexp.rb b/activerecord/lib/arel/nodes/regexp.rb
new file mode 100644
index 0000000000..7c25095569
--- /dev/null
+++ b/activerecord/lib/arel/nodes/regexp.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Regexp < Binary
+ attr_accessor :case_sensitive
+
+ def initialize(left, right, case_sensitive = true)
+ super(left, right)
+ @case_sensitive = case_sensitive
+ end
+ end
+
+ class NotRegexp < Regexp; end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/right_outer_join.rb b/activerecord/lib/arel/nodes/right_outer_join.rb
new file mode 100644
index 0000000000..04ed4aaa78
--- /dev/null
+++ b/activerecord/lib/arel/nodes/right_outer_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class RightOuterJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb
new file mode 100644
index 0000000000..2defe61974
--- /dev/null
+++ b/activerecord/lib/arel/nodes/select_core.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class SelectCore < Arel::Nodes::Node
+ attr_accessor :top, :projections, :wheres, :groups, :windows
+ attr_accessor :havings, :source, :set_quantifier
+
+ def initialize
+ super()
+ @source = JoinSource.new nil
+ @top = nil
+
+ # https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier
+ @set_quantifier = nil
+ @projections = []
+ @wheres = []
+ @groups = []
+ @havings = []
+ @windows = []
+ end
+
+ def from
+ @source.left
+ end
+
+ def from=(value)
+ @source.left = value
+ end
+
+ alias :froms= :from=
+ alias :froms :from
+
+ def initialize_copy(other)
+ super
+ @source = @source.clone if @source
+ @projections = @projections.clone
+ @wheres = @wheres.clone
+ @groups = @groups.clone
+ @havings = @havings.clone
+ @windows = @windows.clone
+ end
+
+ def hash
+ [
+ @source, @top, @set_quantifier, @projections,
+ @wheres, @groups, @havings, @windows
+ ].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.source == other.source &&
+ self.top == other.top &&
+ self.set_quantifier == other.set_quantifier &&
+ self.projections == other.projections &&
+ self.wheres == other.wheres &&
+ self.groups == other.groups &&
+ self.havings == other.havings &&
+ self.windows == other.windows
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/select_statement.rb b/activerecord/lib/arel/nodes/select_statement.rb
new file mode 100644
index 0000000000..eff5dad939
--- /dev/null
+++ b/activerecord/lib/arel/nodes/select_statement.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class SelectStatement < Arel::Nodes::NodeExpression
+ attr_reader :cores
+ attr_accessor :limit, :orders, :lock, :offset, :with
+
+ def initialize(cores = [SelectCore.new])
+ super()
+ @cores = cores
+ @orders = []
+ @limit = nil
+ @lock = nil
+ @offset = nil
+ @with = nil
+ end
+
+ def initialize_copy(other)
+ super
+ @cores = @cores.map { |x| x.clone }
+ @orders = @orders.map { |x| x.clone }
+ end
+
+ def hash
+ [@cores, @orders, @limit, @lock, @offset, @with].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.cores == other.cores &&
+ self.orders == other.orders &&
+ self.limit == other.limit &&
+ self.lock == other.lock &&
+ self.offset == other.offset &&
+ self.with == other.with
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/sql_literal.rb b/activerecord/lib/arel/nodes/sql_literal.rb
new file mode 100644
index 0000000000..d25a8521b7
--- /dev/null
+++ b/activerecord/lib/arel/nodes/sql_literal.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class SqlLiteral < String
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::AliasPredication
+ include Arel::OrderPredications
+
+ def encode_with(coder)
+ coder.scalar = self.to_s
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/string_join.rb b/activerecord/lib/arel/nodes/string_join.rb
new file mode 100644
index 0000000000..86027fcab7
--- /dev/null
+++ b/activerecord/lib/arel/nodes/string_join.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class StringJoin < Arel::Nodes::Join
+ def initialize(left, right = nil)
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/table_alias.rb b/activerecord/lib/arel/nodes/table_alias.rb
new file mode 100644
index 0000000000..f95ca16a3d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/table_alias.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class TableAlias < Arel::Nodes::Binary
+ alias :name :right
+ alias :relation :left
+ alias :table_alias :name
+
+ def [](name)
+ Attribute.new(self, name)
+ end
+
+ def table_name
+ relation.respond_to?(:name) ? relation.name : name
+ end
+
+ def type_cast_for_database(*args)
+ relation.type_cast_for_database(*args)
+ end
+
+ def able_to_type_cast?
+ relation.respond_to?(:able_to_type_cast?) && relation.able_to_type_cast?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/terminal.rb b/activerecord/lib/arel/nodes/terminal.rb
new file mode 100644
index 0000000000..d84c453f1a
--- /dev/null
+++ b/activerecord/lib/arel/nodes/terminal.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Distinct < Arel::Nodes::NodeExpression
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/true.rb b/activerecord/lib/arel/nodes/true.rb
new file mode 100644
index 0000000000..c891012969
--- /dev/null
+++ b/activerecord/lib/arel/nodes/true.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class True < Arel::Nodes::NodeExpression
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/unary.rb b/activerecord/lib/arel/nodes/unary.rb
new file mode 100644
index 0000000000..a3c0045897
--- /dev/null
+++ b/activerecord/lib/arel/nodes/unary.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Unary < Arel::Nodes::NodeExpression
+ attr_accessor :expr
+ alias :value :expr
+
+ def initialize(expr)
+ super()
+ @expr = expr
+ end
+
+ def hash
+ @expr.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.expr == other.expr
+ end
+ alias :== :eql?
+ end
+
+ %w{
+ Bin
+ Cube
+ DistinctOn
+ Group
+ GroupingElement
+ GroupingSet
+ Lateral
+ Limit
+ Lock
+ Not
+ Offset
+ On
+ Ordering
+ RollUp
+ Top
+ }.each do |name|
+ const_set(name, Class.new(Unary))
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/unary_operation.rb b/activerecord/lib/arel/nodes/unary_operation.rb
new file mode 100644
index 0000000000..524282ac84
--- /dev/null
+++ b/activerecord/lib/arel/nodes/unary_operation.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class UnaryOperation < Unary
+ attr_reader :operator
+
+ def initialize(operator, operand)
+ super(operand)
+ @operator = operator
+ end
+ end
+
+ class BitwiseNot < UnaryOperation
+ def initialize(operand)
+ super(:~, operand)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/unqualified_column.rb b/activerecord/lib/arel/nodes/unqualified_column.rb
new file mode 100644
index 0000000000..7c3e0720d7
--- /dev/null
+++ b/activerecord/lib/arel/nodes/unqualified_column.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class UnqualifiedColumn < Arel::Nodes::Unary
+ alias :attribute :expr
+ alias :attribute= :expr=
+
+ def relation
+ @expr.relation
+ end
+
+ def column
+ @expr.column
+ end
+
+ def name
+ @expr.name
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/update_statement.rb b/activerecord/lib/arel/nodes/update_statement.rb
new file mode 100644
index 0000000000..5184b1180f
--- /dev/null
+++ b/activerecord/lib/arel/nodes/update_statement.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class UpdateStatement < Arel::Nodes::Node
+ attr_accessor :relation, :wheres, :values, :orders, :limit
+ attr_accessor :key
+
+ def initialize
+ @relation = nil
+ @wheres = []
+ @values = []
+ @orders = []
+ @limit = nil
+ @key = nil
+ end
+
+ def initialize_copy(other)
+ super
+ @wheres = @wheres.clone
+ @values = @values.clone
+ end
+
+ def hash
+ [@relation, @wheres, @values, @orders, @limit, @key].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.relation == other.relation &&
+ self.wheres == other.wheres &&
+ self.values == other.values &&
+ self.orders == other.orders &&
+ self.limit == other.limit &&
+ self.key == other.key
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/values.rb b/activerecord/lib/arel/nodes/values.rb
new file mode 100644
index 0000000000..650248dc04
--- /dev/null
+++ b/activerecord/lib/arel/nodes/values.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Values < Arel::Nodes::Binary
+ alias :expressions :left
+ alias :expressions= :left=
+ alias :columns :right
+ alias :columns= :right=
+
+ def initialize(exprs, columns = [])
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/values_list.rb b/activerecord/lib/arel/nodes/values_list.rb
new file mode 100644
index 0000000000..27109848e4
--- /dev/null
+++ b/activerecord/lib/arel/nodes/values_list.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class ValuesList < Node
+ attr_reader :rows
+
+ def initialize(rows)
+ @rows = rows
+ super()
+ end
+
+ def hash
+ @rows.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.rows == other.rows
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/window.rb b/activerecord/lib/arel/nodes/window.rb
new file mode 100644
index 0000000000..4916fc7fbe
--- /dev/null
+++ b/activerecord/lib/arel/nodes/window.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Window < Arel::Nodes::Node
+ attr_accessor :orders, :framing, :partitions
+
+ def initialize
+ @orders = []
+ @partitions = []
+ @framing = nil
+ end
+
+ def order(*expr)
+ # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
+ @orders.concat expr.map { |x|
+ String === x || Symbol === x ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def partition(*expr)
+ # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
+ @partitions.concat expr.map { |x|
+ String === x || Symbol === x ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def frame(expr)
+ @framing = expr
+ end
+
+ def rows(expr = nil)
+ if @framing
+ Rows.new(expr)
+ else
+ frame(Rows.new(expr))
+ end
+ end
+
+ def range(expr = nil)
+ if @framing
+ Range.new(expr)
+ else
+ frame(Range.new(expr))
+ end
+ end
+
+ def initialize_copy(other)
+ super
+ @orders = @orders.map { |x| x.clone }
+ end
+
+ def hash
+ [@orders, @framing].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.orders == other.orders &&
+ self.framing == other.framing &&
+ self.partitions == other.partitions
+ end
+ alias :== :eql?
+ end
+
+ class NamedWindow < Window
+ attr_accessor :name
+
+ def initialize(name)
+ super()
+ @name = name
+ end
+
+ def initialize_copy(other)
+ super
+ @name = other.name.clone
+ end
+
+ def hash
+ super ^ @name.hash
+ end
+
+ def eql?(other)
+ super && self.name == other.name
+ end
+ alias :== :eql?
+ end
+
+ class Rows < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+
+ class Range < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+
+ class CurrentRow < Node
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+
+ class Preceding < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+
+ class Following < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/with.rb b/activerecord/lib/arel/nodes/with.rb
new file mode 100644
index 0000000000..157bdcaa08
--- /dev/null
+++ b/activerecord/lib/arel/nodes/with.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class With < Arel::Nodes::Unary
+ alias children expr
+ end
+
+ class WithRecursive < With; end
+ end
+end
diff --git a/activerecord/lib/arel/order_predications.rb b/activerecord/lib/arel/order_predications.rb
new file mode 100644
index 0000000000..d785bbba92
--- /dev/null
+++ b/activerecord/lib/arel/order_predications.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module OrderPredications
+ def asc
+ Nodes::Ascending.new self
+ end
+
+ def desc
+ Nodes::Descending.new self
+ end
+ end
+end
diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb
new file mode 100644
index 0000000000..e83a6f162f
--- /dev/null
+++ b/activerecord/lib/arel/predications.rb
@@ -0,0 +1,241 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Predications
+ def not_eq(other)
+ Nodes::NotEqual.new self, quoted_node(other)
+ end
+
+ def not_eq_any(others)
+ grouping_any :not_eq, others
+ end
+
+ def not_eq_all(others)
+ grouping_all :not_eq, others
+ end
+
+ def eq(other)
+ Nodes::Equality.new self, quoted_node(other)
+ end
+
+ def eq_any(others)
+ grouping_any :eq, others
+ end
+
+ def eq_all(others)
+ grouping_all :eq, quoted_array(others)
+ end
+
+ def between(other)
+ if equals_quoted?(other.begin, -Float::INFINITY)
+ if equals_quoted?(other.end, Float::INFINITY)
+ not_in([])
+ elsif other.exclude_end?
+ lt(other.end)
+ else
+ lteq(other.end)
+ end
+ elsif equals_quoted?(other.end, Float::INFINITY)
+ gteq(other.begin)
+ elsif other.exclude_end?
+ gteq(other.begin).and(lt(other.end))
+ else
+ left = quoted_node(other.begin)
+ right = quoted_node(other.end)
+ Nodes::Between.new(self, left.and(right))
+ end
+ end
+
+ def in(other)
+ case other
+ when Arel::SelectManager
+ Arel::Nodes::In.new(self, other.ast)
+ when Range
+ if $VERBOSE
+ warn <<-eowarn
+Passing a range to `#in` is deprecated. Call `#between`, instead.
+ eowarn
+ end
+ between(other)
+ when Enumerable
+ Nodes::In.new self, quoted_array(other)
+ else
+ Nodes::In.new self, quoted_node(other)
+ end
+ end
+
+ def in_any(others)
+ grouping_any :in, others
+ end
+
+ def in_all(others)
+ grouping_all :in, others
+ end
+
+ def not_between(other)
+ if equals_quoted?(other.begin, -Float::INFINITY)
+ if equals_quoted?(other.end, Float::INFINITY)
+ self.in([])
+ elsif other.exclude_end?
+ gteq(other.end)
+ else
+ gt(other.end)
+ end
+ elsif equals_quoted?(other.end, Float::INFINITY)
+ lt(other.begin)
+ else
+ left = lt(other.begin)
+ right = if other.exclude_end?
+ gteq(other.end)
+ else
+ gt(other.end)
+ end
+ left.or(right)
+ end
+ end
+
+ def not_in(other)
+ case other
+ when Arel::SelectManager
+ Arel::Nodes::NotIn.new(self, other.ast)
+ when Range
+ if $VERBOSE
+ warn <<-eowarn
+Passing a range to `#not_in` is deprecated. Call `#not_between`, instead.
+ eowarn
+ end
+ not_between(other)
+ when Enumerable
+ Nodes::NotIn.new self, quoted_array(other)
+ else
+ Nodes::NotIn.new self, quoted_node(other)
+ end
+ end
+
+ def not_in_any(others)
+ grouping_any :not_in, others
+ end
+
+ def not_in_all(others)
+ grouping_all :not_in, others
+ end
+
+ def matches(other, escape = nil, case_sensitive = false)
+ Nodes::Matches.new self, quoted_node(other), escape, case_sensitive
+ end
+
+ def matches_regexp(other, case_sensitive = true)
+ Nodes::Regexp.new self, quoted_node(other), case_sensitive
+ end
+
+ def matches_any(others, escape = nil, case_sensitive = false)
+ grouping_any :matches, others, escape, case_sensitive
+ end
+
+ def matches_all(others, escape = nil, case_sensitive = false)
+ grouping_all :matches, others, escape, case_sensitive
+ end
+
+ def does_not_match(other, escape = nil, case_sensitive = false)
+ Nodes::DoesNotMatch.new self, quoted_node(other), escape, case_sensitive
+ end
+
+ def does_not_match_regexp(other, case_sensitive = true)
+ Nodes::NotRegexp.new self, quoted_node(other), case_sensitive
+ end
+
+ def does_not_match_any(others, escape = nil)
+ grouping_any :does_not_match, others, escape
+ end
+
+ def does_not_match_all(others, escape = nil)
+ grouping_all :does_not_match, others, escape
+ end
+
+ def gteq(right)
+ Nodes::GreaterThanOrEqual.new self, quoted_node(right)
+ end
+
+ def gteq_any(others)
+ grouping_any :gteq, others
+ end
+
+ def gteq_all(others)
+ grouping_all :gteq, others
+ end
+
+ def gt(right)
+ Nodes::GreaterThan.new self, quoted_node(right)
+ end
+
+ def gt_any(others)
+ grouping_any :gt, others
+ end
+
+ def gt_all(others)
+ grouping_all :gt, others
+ end
+
+ def lt(right)
+ Nodes::LessThan.new self, quoted_node(right)
+ end
+
+ def lt_any(others)
+ grouping_any :lt, others
+ end
+
+ def lt_all(others)
+ grouping_all :lt, others
+ end
+
+ def lteq(right)
+ Nodes::LessThanOrEqual.new self, quoted_node(right)
+ end
+
+ def lteq_any(others)
+ grouping_any :lteq, others
+ end
+
+ def lteq_all(others)
+ grouping_all :lteq, others
+ end
+
+ def when(right)
+ Nodes::Case.new(self).when quoted_node(right)
+ end
+
+ def concat(other)
+ Nodes::Concat.new self, other
+ end
+
+ private
+
+ def grouping_any(method_id, others, *extras)
+ nodes = others.map { |expr| send(method_id, expr, *extras) }
+ Nodes::Grouping.new nodes.inject { |memo, node|
+ Nodes::Or.new(memo, node)
+ }
+ end
+
+ def grouping_all(method_id, others, *extras)
+ nodes = others.map { |expr| send(method_id, expr, *extras) }
+ Nodes::Grouping.new Nodes::And.new(nodes)
+ end
+
+ def quoted_node(other)
+ Nodes.build_quoted(other, self)
+ end
+
+ def quoted_array(others)
+ others.map { |v| quoted_node(v) }
+ end
+
+ def equals_quoted?(maybe_quoted, value)
+ if maybe_quoted.is_a?(Nodes::Quoted)
+ maybe_quoted.val == value
+ else
+ maybe_quoted == value
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb
new file mode 100644
index 0000000000..22a04b00c6
--- /dev/null
+++ b/activerecord/lib/arel/select_manager.rb
@@ -0,0 +1,273 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class SelectManager < Arel::TreeManager
+ include Arel::Crud
+
+ STRING_OR_SYMBOL_CLASS = [Symbol, String]
+
+ def initialize(table = nil)
+ super()
+ @ast = Nodes::SelectStatement.new
+ @ctx = @ast.cores.last
+ from table
+ end
+
+ def initialize_copy(other)
+ super
+ @ctx = @ast.cores.last
+ end
+
+ def limit
+ @ast.limit && @ast.limit.expr
+ end
+ alias :taken :limit
+
+ def constraints
+ @ctx.wheres
+ end
+
+ def offset
+ @ast.offset && @ast.offset.expr
+ end
+
+ def skip(amount)
+ if amount
+ @ast.offset = Nodes::Offset.new(amount)
+ else
+ @ast.offset = nil
+ end
+ self
+ end
+ alias :offset= :skip
+
+ ###
+ # Produces an Arel::Nodes::Exists node
+ def exists
+ Arel::Nodes::Exists.new @ast
+ end
+
+ def as(other)
+ create_table_alias grouping(@ast), Nodes::SqlLiteral.new(other)
+ end
+
+ def lock(locking = Arel.sql("FOR UPDATE"))
+ case locking
+ when true
+ locking = Arel.sql("FOR UPDATE")
+ when Arel::Nodes::SqlLiteral
+ when String
+ locking = Arel.sql locking
+ end
+
+ @ast.lock = Nodes::Lock.new(locking)
+ self
+ end
+
+ def locked
+ @ast.lock
+ end
+
+ def on(*exprs)
+ @ctx.source.right.last.right = Nodes::On.new(collapse(exprs))
+ self
+ end
+
+ def group(*columns)
+ columns.each do |column|
+ # FIXME: backwards compat
+ column = Nodes::SqlLiteral.new(column) if String === column
+ column = Nodes::SqlLiteral.new(column.to_s) if Symbol === column
+
+ @ctx.groups.push Nodes::Group.new column
+ end
+ self
+ end
+
+ def from(table)
+ table = Nodes::SqlLiteral.new(table) if String === table
+
+ case table
+ when Nodes::Join
+ @ctx.source.right << table
+ else
+ @ctx.source.left = table
+ end
+
+ self
+ end
+
+ def froms
+ @ast.cores.map { |x| x.from }.compact
+ end
+
+ def join(relation, klass = Nodes::InnerJoin)
+ return self unless relation
+
+ case relation
+ when String, Nodes::SqlLiteral
+ raise EmptyJoinError if relation.empty?
+ klass = Nodes::StringJoin
+ end
+
+ @ctx.source.right << create_join(relation, nil, klass)
+ self
+ end
+
+ def outer_join(relation)
+ join(relation, Nodes::OuterJoin)
+ end
+
+ def having(expr)
+ @ctx.havings << expr
+ self
+ end
+
+ def window(name)
+ window = Nodes::NamedWindow.new(name)
+ @ctx.windows.push window
+ window
+ end
+
+ def project(*projections)
+ # FIXME: converting these to SQLLiterals is probably not good, but
+ # rails tests require it.
+ @ctx.projections.concat projections.map { |x|
+ STRING_OR_SYMBOL_CLASS.include?(x.class) ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def projections
+ @ctx.projections
+ end
+
+ def projections=(projections)
+ @ctx.projections = projections
+ end
+
+ def distinct(value = true)
+ if value
+ @ctx.set_quantifier = Arel::Nodes::Distinct.new
+ else
+ @ctx.set_quantifier = nil
+ end
+ self
+ end
+
+ def distinct_on(value)
+ if value
+ @ctx.set_quantifier = Arel::Nodes::DistinctOn.new(value)
+ else
+ @ctx.set_quantifier = nil
+ end
+ self
+ end
+
+ def order(*expr)
+ # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
+ @ast.orders.concat expr.map { |x|
+ STRING_OR_SYMBOL_CLASS.include?(x.class) ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def orders
+ @ast.orders
+ end
+
+ def where_sql(engine = Table.engine)
+ return if @ctx.wheres.empty?
+
+ viz = Visitors::WhereSql.new(engine.connection.visitor, engine.connection)
+ Nodes::SqlLiteral.new viz.accept(@ctx, Collectors::SQLString.new).value
+ end
+
+ def union(operation, other = nil)
+ if other
+ node_class = Nodes.const_get("Union#{operation.to_s.capitalize}")
+ else
+ other = operation
+ node_class = Nodes::Union
+ end
+
+ node_class.new self.ast, other.ast
+ end
+
+ def intersect(other)
+ Nodes::Intersect.new ast, other.ast
+ end
+
+ def except(other)
+ Nodes::Except.new ast, other.ast
+ end
+ alias :minus :except
+
+ def lateral(table_name = nil)
+ base = table_name.nil? ? ast : as(table_name)
+ Nodes::Lateral.new(base)
+ end
+
+ def with(*subqueries)
+ if subqueries.first.is_a? Symbol
+ node_class = Nodes.const_get("With#{subqueries.shift.to_s.capitalize}")
+ else
+ node_class = Nodes::With
+ end
+ @ast.with = node_class.new(subqueries.flatten)
+
+ self
+ end
+
+ def take(limit)
+ if limit
+ @ast.limit = Nodes::Limit.new(limit)
+ @ctx.top = Nodes::Top.new(limit)
+ else
+ @ast.limit = nil
+ @ctx.top = nil
+ end
+ self
+ end
+ alias limit= take
+
+ def join_sources
+ @ctx.source.right
+ end
+
+ def source
+ @ctx.source
+ end
+
+ class Row < Struct.new(:data) # :nodoc:
+ def id
+ data["id"]
+ end
+
+ def method_missing(name, *args)
+ name = name.to_s
+ return data[name] if data.key?(name)
+ super
+ end
+ end
+
+ private
+ def collapse(exprs, existing = nil)
+ exprs = exprs.unshift(existing.expr) if existing
+ exprs = exprs.compact.map { |expr|
+ if String === expr
+ # FIXME: Don't do this automatically
+ Arel.sql(expr)
+ else
+ expr
+ end
+ }
+
+ if exprs.length == 1
+ exprs.first
+ else
+ create_and exprs
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/table.rb b/activerecord/lib/arel/table.rb
new file mode 100644
index 0000000000..686fcdf962
--- /dev/null
+++ b/activerecord/lib/arel/table.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class Table
+ include Arel::Crud
+ include Arel::FactoryMethods
+
+ @engine = nil
+ class << self; attr_accessor :engine; end
+
+ attr_accessor :name, :table_alias
+
+ # TableAlias and Table both have a #table_name which is the name of the underlying table
+ alias :table_name :name
+
+ def initialize(name, as: nil, type_caster: nil)
+ @name = name.to_s
+ @type_caster = type_caster
+
+ # Sometime AR sends an :as parameter to table, to let the table know
+ # that it is an Alias. We may want to override new, and return a
+ # TableAlias node?
+ if as.to_s == @name
+ as = nil
+ end
+ @table_alias = as
+ end
+
+ def alias(name = "#{self.name}_2")
+ Nodes::TableAlias.new(self, name)
+ end
+
+ def from
+ SelectManager.new(self)
+ end
+
+ def join(relation, klass = Nodes::InnerJoin)
+ return from unless relation
+
+ case relation
+ when String, Nodes::SqlLiteral
+ raise EmptyJoinError if relation.empty?
+ klass = Nodes::StringJoin
+ end
+
+ from.join(relation, klass)
+ end
+
+ def outer_join(relation)
+ join(relation, Nodes::OuterJoin)
+ end
+
+ def group(*columns)
+ from.group(*columns)
+ end
+
+ def order(*expr)
+ from.order(*expr)
+ end
+
+ def where(condition)
+ from.where condition
+ end
+
+ def project(*things)
+ from.project(*things)
+ end
+
+ def take(amount)
+ from.take amount
+ end
+
+ def skip(amount)
+ from.skip amount
+ end
+
+ def having(expr)
+ from.having expr
+ end
+
+ def [](name)
+ ::Arel::Attribute.new self, name
+ end
+
+ def hash
+ # Perf note: aliases and table alias is excluded from the hash
+ # aliases can have a loop back to this table breaking hashes in parent
+ # relations, for the vast majority of cases @name is unique to a query
+ @name.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.name == other.name &&
+ self.table_alias == other.table_alias
+ end
+ alias :== :eql?
+
+ def type_cast_for_database(attribute_name, value)
+ type_caster.type_cast_for_database(attribute_name, value)
+ end
+
+ def able_to_type_cast?
+ !type_caster.nil?
+ end
+
+ protected
+
+ attr_reader :type_caster
+ end
+end
diff --git a/activerecord/lib/arel/tree_manager.rb b/activerecord/lib/arel/tree_manager.rb
new file mode 100644
index 0000000000..ed47b09a37
--- /dev/null
+++ b/activerecord/lib/arel/tree_manager.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class TreeManager
+ include Arel::FactoryMethods
+
+ attr_reader :ast
+
+ def initialize
+ @ctx = nil
+ end
+
+ def to_dot
+ collector = Arel::Collectors::PlainString.new
+ collector = Visitors::Dot.new.accept @ast, collector
+ collector.value
+ end
+
+ def to_sql(engine = Table.engine)
+ collector = Arel::Collectors::SQLString.new
+ collector = engine.connection.visitor.accept @ast, collector
+ collector.value
+ end
+
+ def initialize_copy(other)
+ super
+ @ast = @ast.clone
+ end
+
+ def where(expr)
+ if Arel::TreeManager === expr
+ expr = expr.ast
+ end
+ @ctx.wheres << expr
+ self
+ end
+ end
+end
diff --git a/activerecord/lib/arel/update_manager.rb b/activerecord/lib/arel/update_manager.rb
new file mode 100644
index 0000000000..fe444343ba
--- /dev/null
+++ b/activerecord/lib/arel/update_manager.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class UpdateManager < Arel::TreeManager
+ def initialize
+ super
+ @ast = Nodes::UpdateStatement.new
+ @ctx = @ast
+ end
+
+ def take(limit)
+ @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit
+ self
+ end
+
+ def key=(key)
+ @ast.key = Nodes.build_quoted(key)
+ end
+
+ def key
+ @ast.key
+ end
+
+ def order(*expr)
+ @ast.orders = expr
+ self
+ end
+
+ ###
+ # UPDATE +table+
+ def table(table)
+ @ast.relation = table
+ self
+ end
+
+ def wheres=(exprs)
+ @ast.wheres = exprs
+ end
+
+ def where(expr)
+ @ast.wheres << expr
+ self
+ end
+
+ def set(values)
+ if String === values
+ @ast.values = [values]
+ else
+ @ast.values = values.map { |column, value|
+ Nodes::Assignment.new(
+ Nodes::UnqualifiedColumn.new(column),
+ value
+ )
+ }
+ end
+ self
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors.rb b/activerecord/lib/arel/visitors.rb
new file mode 100644
index 0000000000..e350f52e65
--- /dev/null
+++ b/activerecord/lib/arel/visitors.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require "arel/visitors/visitor"
+require "arel/visitors/depth_first"
+require "arel/visitors/to_sql"
+require "arel/visitors/sqlite"
+require "arel/visitors/postgresql"
+require "arel/visitors/mysql"
+require "arel/visitors/mssql"
+require "arel/visitors/oracle"
+require "arel/visitors/oracle12"
+require "arel/visitors/where_sql"
+require "arel/visitors/dot"
+require "arel/visitors/ibm_db"
+require "arel/visitors/informix"
+
+module Arel # :nodoc: all
+ module Visitors
+ end
+end
diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb
new file mode 100644
index 0000000000..bcf8f8f980
--- /dev/null
+++ b/activerecord/lib/arel/visitors/depth_first.rb
@@ -0,0 +1,200 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class DepthFirst < Arel::Visitors::Visitor
+ def initialize(block = nil)
+ @block = block || Proc.new
+ super()
+ end
+
+ private
+
+ def visit(o)
+ super
+ @block.call o
+ end
+
+ def unary(o)
+ visit o.expr
+ end
+ alias :visit_Arel_Nodes_Else :unary
+ alias :visit_Arel_Nodes_Group :unary
+ alias :visit_Arel_Nodes_Cube :unary
+ alias :visit_Arel_Nodes_RollUp :unary
+ alias :visit_Arel_Nodes_GroupingSet :unary
+ alias :visit_Arel_Nodes_GroupingElement :unary
+ alias :visit_Arel_Nodes_Grouping :unary
+ alias :visit_Arel_Nodes_Having :unary
+ alias :visit_Arel_Nodes_Lateral :unary
+ alias :visit_Arel_Nodes_Limit :unary
+ alias :visit_Arel_Nodes_Not :unary
+ alias :visit_Arel_Nodes_Offset :unary
+ alias :visit_Arel_Nodes_On :unary
+ alias :visit_Arel_Nodes_Ordering :unary
+ alias :visit_Arel_Nodes_Ascending :unary
+ alias :visit_Arel_Nodes_Descending :unary
+ alias :visit_Arel_Nodes_Top :unary
+ alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+
+ def function(o)
+ visit o.expressions
+ visit o.alias
+ visit o.distinct
+ end
+ alias :visit_Arel_Nodes_Avg :function
+ alias :visit_Arel_Nodes_Exists :function
+ alias :visit_Arel_Nodes_Max :function
+ alias :visit_Arel_Nodes_Min :function
+ alias :visit_Arel_Nodes_Sum :function
+
+ def visit_Arel_Nodes_NamedFunction(o)
+ visit o.name
+ visit o.expressions
+ visit o.distinct
+ visit o.alias
+ end
+
+ def visit_Arel_Nodes_Count(o)
+ visit o.expressions
+ visit o.alias
+ visit o.distinct
+ end
+
+ def visit_Arel_Nodes_Case(o)
+ visit o.case
+ visit o.conditions
+ visit o.default
+ end
+
+ def nary(o)
+ o.children.each { |child| visit child }
+ end
+ alias :visit_Arel_Nodes_And :nary
+
+ def binary(o)
+ visit o.left
+ visit o.right
+ end
+ alias :visit_Arel_Nodes_As :binary
+ alias :visit_Arel_Nodes_Assignment :binary
+ alias :visit_Arel_Nodes_Between :binary
+ alias :visit_Arel_Nodes_Concat :binary
+ alias :visit_Arel_Nodes_DeleteStatement :binary
+ alias :visit_Arel_Nodes_DoesNotMatch :binary
+ alias :visit_Arel_Nodes_Equality :binary
+ alias :visit_Arel_Nodes_FullOuterJoin :binary
+ alias :visit_Arel_Nodes_GreaterThan :binary
+ alias :visit_Arel_Nodes_GreaterThanOrEqual :binary
+ alias :visit_Arel_Nodes_In :binary
+ alias :visit_Arel_Nodes_InfixOperation :binary
+ alias :visit_Arel_Nodes_JoinSource :binary
+ alias :visit_Arel_Nodes_InnerJoin :binary
+ alias :visit_Arel_Nodes_LessThan :binary
+ alias :visit_Arel_Nodes_LessThanOrEqual :binary
+ alias :visit_Arel_Nodes_Matches :binary
+ alias :visit_Arel_Nodes_NotEqual :binary
+ alias :visit_Arel_Nodes_NotIn :binary
+ alias :visit_Arel_Nodes_NotRegexp :binary
+ alias :visit_Arel_Nodes_Or :binary
+ alias :visit_Arel_Nodes_OuterJoin :binary
+ alias :visit_Arel_Nodes_Regexp :binary
+ alias :visit_Arel_Nodes_RightOuterJoin :binary
+ alias :visit_Arel_Nodes_TableAlias :binary
+ alias :visit_Arel_Nodes_Values :binary
+ alias :visit_Arel_Nodes_When :binary
+
+ def visit_Arel_Nodes_StringJoin(o)
+ visit o.left
+ end
+
+ def visit_Arel_Attribute(o)
+ visit o.relation
+ visit o.name
+ end
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_String :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Decimal :visit_Arel_Attribute
+
+ def visit_Arel_Table(o)
+ visit o.name
+ end
+
+ def terminal(o)
+ end
+ alias :visit_ActiveSupport_Multibyte_Chars :terminal
+ alias :visit_ActiveSupport_StringInquirer :terminal
+ alias :visit_Arel_Nodes_Lock :terminal
+ alias :visit_Arel_Nodes_Node :terminal
+ alias :visit_Arel_Nodes_SqlLiteral :terminal
+ alias :visit_Arel_Nodes_BindParam :terminal
+ alias :visit_Arel_Nodes_Window :terminal
+ alias :visit_Arel_Nodes_True :terminal
+ alias :visit_Arel_Nodes_False :terminal
+ alias :visit_BigDecimal :terminal
+ alias :visit_Bignum :terminal
+ alias :visit_Class :terminal
+ alias :visit_Date :terminal
+ alias :visit_DateTime :terminal
+ alias :visit_FalseClass :terminal
+ alias :visit_Fixnum :terminal
+ alias :visit_Float :terminal
+ alias :visit_Integer :terminal
+ alias :visit_NilClass :terminal
+ alias :visit_String :terminal
+ alias :visit_Symbol :terminal
+ alias :visit_Time :terminal
+ alias :visit_TrueClass :terminal
+
+ def visit_Arel_Nodes_InsertStatement(o)
+ visit o.relation
+ visit o.columns
+ visit o.values
+ end
+
+ def visit_Arel_Nodes_SelectCore(o)
+ visit o.projections
+ visit o.source
+ visit o.wheres
+ visit o.groups
+ visit o.windows
+ visit o.havings
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o)
+ visit o.cores
+ visit o.orders
+ visit o.limit
+ visit o.lock
+ visit o.offset
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o)
+ visit o.relation
+ visit o.values
+ visit o.wheres
+ visit o.orders
+ visit o.limit
+ end
+
+ def visit_Array(o)
+ o.each { |i| visit i }
+ end
+ alias :visit_Set :visit_Array
+
+ def visit_Hash(o)
+ o.each { |k, v| visit(k); visit(v) }
+ end
+
+ DISPATCH = dispatch_cache
+
+ def get_dispatch_cache
+ DISPATCH
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb
new file mode 100644
index 0000000000..d352b81914
--- /dev/null
+++ b/activerecord/lib/arel/visitors/dot.rb
@@ -0,0 +1,292 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Dot < Arel::Visitors::Visitor
+ class Node # :nodoc:
+ attr_accessor :name, :id, :fields
+
+ def initialize(name, id, fields = [])
+ @name = name
+ @id = id
+ @fields = fields
+ end
+ end
+
+ class Edge < Struct.new :name, :from, :to # :nodoc:
+ end
+
+ def initialize
+ super()
+ @nodes = []
+ @edges = []
+ @node_stack = []
+ @edge_stack = []
+ @seen = {}
+ end
+
+ def accept(object, collector)
+ visit object
+ collector << to_dot
+ end
+
+ private
+
+ def visit_Arel_Nodes_Ordering(o)
+ visit_edge o, "expr"
+ end
+
+ def visit_Arel_Nodes_TableAlias(o)
+ visit_edge o, "name"
+ visit_edge o, "relation"
+ end
+
+ def visit_Arel_Nodes_Count(o)
+ visit_edge o, "expressions"
+ visit_edge o, "distinct"
+ end
+
+ def visit_Arel_Nodes_Values(o)
+ visit_edge o, "expressions"
+ end
+
+ def visit_Arel_Nodes_StringJoin(o)
+ visit_edge o, "left"
+ end
+
+ def visit_Arel_Nodes_InnerJoin(o)
+ visit_edge o, "left"
+ visit_edge o, "right"
+ end
+ alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_InnerJoin
+ alias :visit_Arel_Nodes_OuterJoin :visit_Arel_Nodes_InnerJoin
+ alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_InnerJoin
+
+ def visit_Arel_Nodes_DeleteStatement(o)
+ visit_edge o, "relation"
+ visit_edge o, "wheres"
+ end
+
+ def unary(o)
+ visit_edge o, "expr"
+ end
+ alias :visit_Arel_Nodes_Group :unary
+ alias :visit_Arel_Nodes_Cube :unary
+ alias :visit_Arel_Nodes_RollUp :unary
+ alias :visit_Arel_Nodes_GroupingSet :unary
+ alias :visit_Arel_Nodes_GroupingElement :unary
+ alias :visit_Arel_Nodes_Grouping :unary
+ alias :visit_Arel_Nodes_Having :unary
+ alias :visit_Arel_Nodes_Limit :unary
+ alias :visit_Arel_Nodes_Not :unary
+ alias :visit_Arel_Nodes_Offset :unary
+ alias :visit_Arel_Nodes_On :unary
+ alias :visit_Arel_Nodes_Top :unary
+ alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+ alias :visit_Arel_Nodes_Preceding :unary
+ alias :visit_Arel_Nodes_Following :unary
+ alias :visit_Arel_Nodes_Rows :unary
+ alias :visit_Arel_Nodes_Range :unary
+
+ def window(o)
+ visit_edge o, "partitions"
+ visit_edge o, "orders"
+ visit_edge o, "framing"
+ end
+ alias :visit_Arel_Nodes_Window :window
+
+ def named_window(o)
+ visit_edge o, "partitions"
+ visit_edge o, "orders"
+ visit_edge o, "framing"
+ visit_edge o, "name"
+ end
+ alias :visit_Arel_Nodes_NamedWindow :named_window
+
+ def function(o)
+ visit_edge o, "expressions"
+ visit_edge o, "distinct"
+ visit_edge o, "alias"
+ end
+ alias :visit_Arel_Nodes_Exists :function
+ alias :visit_Arel_Nodes_Min :function
+ alias :visit_Arel_Nodes_Max :function
+ alias :visit_Arel_Nodes_Avg :function
+ alias :visit_Arel_Nodes_Sum :function
+
+ def extract(o)
+ visit_edge o, "expressions"
+ visit_edge o, "alias"
+ end
+ alias :visit_Arel_Nodes_Extract :extract
+
+ def visit_Arel_Nodes_NamedFunction(o)
+ visit_edge o, "name"
+ visit_edge o, "expressions"
+ visit_edge o, "distinct"
+ visit_edge o, "alias"
+ end
+
+ def visit_Arel_Nodes_InsertStatement(o)
+ visit_edge o, "relation"
+ visit_edge o, "columns"
+ visit_edge o, "values"
+ end
+
+ def visit_Arel_Nodes_SelectCore(o)
+ visit_edge o, "source"
+ visit_edge o, "projections"
+ visit_edge o, "wheres"
+ visit_edge o, "windows"
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o)
+ visit_edge o, "cores"
+ visit_edge o, "limit"
+ visit_edge o, "orders"
+ visit_edge o, "offset"
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o)
+ visit_edge o, "relation"
+ visit_edge o, "wheres"
+ visit_edge o, "values"
+ end
+
+ def visit_Arel_Table(o)
+ visit_edge o, "name"
+ end
+
+ def visit_Arel_Nodes_Casted(o)
+ visit_edge o, "val"
+ visit_edge o, "attribute"
+ end
+
+ def visit_Arel_Attribute(o)
+ visit_edge o, "relation"
+ visit_edge o, "name"
+ end
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_String :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute
+
+ def nary(o)
+ o.children.each_with_index do |x, i|
+ edge(i) { visit x }
+ end
+ end
+ alias :visit_Arel_Nodes_And :nary
+
+ def binary(o)
+ visit_edge o, "left"
+ visit_edge o, "right"
+ end
+ alias :visit_Arel_Nodes_As :binary
+ alias :visit_Arel_Nodes_Assignment :binary
+ alias :visit_Arel_Nodes_Between :binary
+ alias :visit_Arel_Nodes_Concat :binary
+ alias :visit_Arel_Nodes_DoesNotMatch :binary
+ alias :visit_Arel_Nodes_Equality :binary
+ alias :visit_Arel_Nodes_GreaterThan :binary
+ alias :visit_Arel_Nodes_GreaterThanOrEqual :binary
+ alias :visit_Arel_Nodes_In :binary
+ alias :visit_Arel_Nodes_JoinSource :binary
+ alias :visit_Arel_Nodes_LessThan :binary
+ alias :visit_Arel_Nodes_LessThanOrEqual :binary
+ alias :visit_Arel_Nodes_Matches :binary
+ alias :visit_Arel_Nodes_NotEqual :binary
+ alias :visit_Arel_Nodes_NotIn :binary
+ alias :visit_Arel_Nodes_Or :binary
+ alias :visit_Arel_Nodes_Over :binary
+
+ def visit_String(o)
+ @node_stack.last.fields << o
+ end
+ alias :visit_Time :visit_String
+ alias :visit_Date :visit_String
+ alias :visit_DateTime :visit_String
+ alias :visit_NilClass :visit_String
+ alias :visit_TrueClass :visit_String
+ alias :visit_FalseClass :visit_String
+ alias :visit_Integer :visit_String
+ alias :visit_Fixnum :visit_String
+ alias :visit_BigDecimal :visit_String
+ alias :visit_Float :visit_String
+ alias :visit_Symbol :visit_String
+ alias :visit_Arel_Nodes_SqlLiteral :visit_String
+
+ def visit_Arel_Nodes_BindParam(o); end
+
+ def visit_Hash(o)
+ o.each_with_index do |pair, i|
+ edge("pair_#{i}") { visit pair }
+ end
+ end
+
+ def visit_Array(o)
+ o.each_with_index do |x, i|
+ edge(i) { visit x }
+ end
+ end
+ alias :visit_Set :visit_Array
+
+ def visit_edge(o, method)
+ edge(method) { visit o.send(method) }
+ end
+
+ def visit(o)
+ if node = @seen[o.object_id]
+ @edge_stack.last.to = node
+ return
+ end
+
+ node = Node.new(o.class.name, o.object_id)
+ @seen[node.id] = node
+ @nodes << node
+ with_node node do
+ super
+ end
+ end
+
+ def edge(name)
+ edge = Edge.new(name, @node_stack.last)
+ @edge_stack.push edge
+ @edges << edge
+ yield
+ @edge_stack.pop
+ end
+
+ def with_node(node)
+ if edge = @edge_stack.last
+ edge.to = node
+ end
+
+ @node_stack.push node
+ yield
+ @node_stack.pop
+ end
+
+ def quote(string)
+ string.to_s.gsub('"', '\"')
+ end
+
+ def to_dot
+ "digraph \"Arel\" {\nnode [width=0.375,height=0.25,shape=record];\n" +
+ @nodes.map { |node|
+ label = "<f0>#{node.name}"
+
+ node.fields.each_with_index do |field, i|
+ label += "|<f#{i + 1}>#{quote field}"
+ end
+
+ "#{node.id} [label=\"#{label}\"];"
+ }.join("\n") + "\n" + @edges.map { |edge|
+ "#{edge.from.id} -> #{edge.to.id} [label=\"#{edge.name}\"];"
+ }.join("\n") + "\n}"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb
new file mode 100644
index 0000000000..0a06aef60b
--- /dev/null
+++ b/activerecord/lib/arel/visitors/ibm_db.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class IBM_DB < Arel::Visitors::ToSql
+ private
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "FETCH FIRST "
+ collector = visit o.expr, collector
+ collector << " ROWS ONLY"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/informix.rb b/activerecord/lib/arel/visitors/informix.rb
new file mode 100644
index 0000000000..0a9713794e
--- /dev/null
+++ b/activerecord/lib/arel/visitors/informix.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Informix < Arel::Visitors::ToSql
+ private
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ collector << "SELECT "
+ collector = maybe_visit o.offset, collector
+ collector = maybe_visit o.limit, collector
+ collector = o.cores.inject(collector) { |c, x|
+ visit_Arel_Nodes_SelectCore x, c
+ }
+ if o.orders.any?
+ collector << "ORDER BY "
+ collector = inject_join o.orders, collector, ", "
+ end
+ collector = maybe_visit o.lock, collector
+ end
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector = inject_join o.projections, collector, ", "
+ if o.source && !o.source.empty?
+ collector << " FROM "
+ collector = visit o.source, collector
+ end
+
+ if o.wheres.any?
+ collector << " WHERE "
+ collector = inject_join o.wheres, collector, " AND "
+ end
+
+ if o.groups.any?
+ collector << "GROUP BY "
+ collector = inject_join o.groups, collector, ", "
+ end
+
+ if o.havings.any?
+ collector << " HAVING "
+ collector = inject_join o.havings, collector, " AND "
+ end
+ collector
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "SKIP "
+ visit o.expr, collector
+ end
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "FIRST "
+ visit o.expr, collector
+ collector << " "
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb
new file mode 100644
index 0000000000..9aedc51d15
--- /dev/null
+++ b/activerecord/lib/arel/visitors/mssql.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class MSSQL < Arel::Visitors::ToSql
+ RowNumber = Struct.new :children
+
+ def initialize(*)
+ @primary_keys = {}
+ super
+ end
+
+ private
+
+ # `top` wouldn't really work here. I.e. User.select("distinct first_name").limit(10) would generate
+ # "select top 10 distinct first_name from users", which is invalid query! it should be
+ # "select distinct top 10 first_name from users"
+ def visit_Arel_Nodes_Top(o)
+ ""
+ end
+
+ def visit_Arel_Visitors_MSSQL_RowNumber(o, collector)
+ collector << "ROW_NUMBER() OVER (ORDER BY "
+ inject_join(o.children, collector, ", ") << ") as _row_num"
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ if !o.limit && !o.offset
+ return super
+ end
+
+ is_select_count = false
+ o.cores.each { |x|
+ core_order_by = row_num_literal determine_order_by(o.orders, x)
+ if select_count? x
+ x.projections = [core_order_by]
+ is_select_count = true
+ else
+ x.projections << core_order_by
+ end
+ }
+
+ if is_select_count
+ # fixme count distinct wouldn't work with limit or offset
+ collector << "SELECT COUNT(1) as count_id FROM ("
+ end
+
+ collector << "SELECT _t.* FROM ("
+ collector = o.cores.inject(collector) { |c, x|
+ visit_Arel_Nodes_SelectCore x, c
+ }
+ collector << ") as _t WHERE #{get_offset_limit_clause(o)}"
+
+ if is_select_count
+ collector << ") AS subquery"
+ else
+ collector
+ end
+ end
+
+ def get_offset_limit_clause(o)
+ first_row = o.offset ? o.offset.expr.to_i + 1 : 1
+ last_row = o.limit ? o.limit.expr.to_i - 1 + first_row : nil
+ if last_row
+ " _row_num BETWEEN #{first_row} AND #{last_row}"
+ else
+ " _row_num >= #{first_row}"
+ end
+ end
+
+ def visit_Arel_Nodes_DeleteStatement(o, collector)
+ collector << "DELETE "
+ if o.limit
+ collector << "TOP ("
+ visit o.limit.expr, collector
+ collector << ") "
+ end
+ collector << "FROM "
+ collector = visit o.relation, collector
+ if o.wheres.any?
+ collector << " WHERE "
+ inject_join o.wheres, collector, AND
+ else
+ collector
+ end
+ end
+
+ def determine_order_by(orders, x)
+ if orders.any?
+ orders
+ elsif x.groups.any?
+ x.groups
+ else
+ pk = find_left_table_pk(x.froms)
+ pk ? [pk] : []
+ end
+ end
+
+ def row_num_literal(order_by)
+ RowNumber.new order_by
+ end
+
+ def select_count?(x)
+ x.projections.length == 1 && Arel::Nodes::Count === x.projections.first
+ end
+
+ # FIXME raise exception of there is no pk?
+ def find_left_table_pk(o)
+ if o.kind_of?(Arel::Nodes::Join)
+ find_left_table_pk(o.left)
+ elsif o.instance_of?(Arel::Table)
+ find_primary_key(o)
+ end
+ end
+
+ def find_primary_key(o)
+ @primary_keys[o.name] ||= begin
+ primary_key_name = @connection.primary_key(o.name)
+ # some tables might be without primary key
+ primary_key_name && o[primary_key_name]
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb
new file mode 100644
index 0000000000..37bfb661f0
--- /dev/null
+++ b/activerecord/lib/arel/visitors/mysql.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class MySQL < Arel::Visitors::ToSql
+ private
+ def visit_Arel_Nodes_Union(o, collector, suppress_parens = false)
+ unless suppress_parens
+ collector << "( "
+ end
+
+ collector = case o.left
+ when Arel::Nodes::Union
+ visit_Arel_Nodes_Union o.left, collector, true
+ else
+ visit o.left, collector
+ end
+
+ collector << " UNION "
+
+ collector = case o.right
+ when Arel::Nodes::Union
+ visit_Arel_Nodes_Union o.right, collector, true
+ else
+ visit o.right, collector
+ end
+
+ if suppress_parens
+ collector
+ else
+ collector << " )"
+ end
+ end
+
+ def visit_Arel_Nodes_Bin(o, collector)
+ collector << "BINARY "
+ visit o.expr, collector
+ end
+
+ ###
+ # :'(
+ # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ if o.offset && !o.limit
+ o.limit = Arel::Nodes::Limit.new(18446744073709551615)
+ end
+ super
+ end
+
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ o.froms ||= Arel.sql("DUAL")
+ super
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ collector << "UPDATE "
+ collector = visit o.relation, collector
+
+ unless o.values.empty?
+ collector << " SET "
+ collector = inject_join o.values, collector, ", "
+ end
+
+ unless o.wheres.empty?
+ collector << " WHERE "
+ collector = inject_join o.wheres, collector, " AND "
+ end
+
+ unless o.orders.empty?
+ collector << " ORDER BY "
+ collector = inject_join o.orders, collector, ", "
+ end
+
+ maybe_visit o.limit, collector
+ end
+
+ def visit_Arel_Nodes_Concat(o, collector)
+ collector << " CONCAT("
+ visit o.left, collector
+ collector << ", "
+ visit o.right, collector
+ collector << ") "
+ collector
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb
new file mode 100644
index 0000000000..30a1529d46
--- /dev/null
+++ b/activerecord/lib/arel/visitors/oracle.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Oracle < Arel::Visitors::ToSql
+ private
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ o = order_hacks(o)
+
+ # if need to select first records without ORDER BY and GROUP BY and without DISTINCT
+ # then can use simple ROWNUM in WHERE clause
+ if o.limit && o.orders.empty? && o.cores.first.groups.empty? && !o.offset && o.cores.first.set_quantifier.class.to_s !~ /Distinct/
+ o.cores.last.wheres.push Nodes::LessThanOrEqual.new(
+ Nodes::SqlLiteral.new("ROWNUM"), o.limit.expr
+ )
+ return super
+ end
+
+ if o.limit && o.offset
+ o = o.dup
+ limit = o.limit.expr
+ offset = o.offset
+ o.offset = nil
+ collector << "
+ SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM ("
+
+ collector = super(o, collector)
+
+ if offset.expr.is_a? Nodes::BindParam
+ collector << ") raw_sql_ WHERE rownum <= ("
+ collector = visit offset.expr, collector
+ collector << " + "
+ collector = visit limit, collector
+ collector << ") ) WHERE raw_rnum_ > "
+ collector = visit offset.expr, collector
+ return collector
+ else
+ collector << ") raw_sql_
+ WHERE rownum <= #{offset.expr.to_i + limit}
+ )
+ WHERE "
+ return visit(offset, collector)
+ end
+ end
+
+ if o.limit
+ o = o.dup
+ limit = o.limit.expr
+ collector << "SELECT * FROM ("
+ collector = super(o, collector)
+ collector << ") WHERE ROWNUM <= "
+ return visit limit, collector
+ end
+
+ if o.offset
+ o = o.dup
+ offset = o.offset
+ o.offset = nil
+ collector << "SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM ("
+ collector = super(o, collector)
+ collector << ") raw_sql_
+ )
+ WHERE "
+ return visit offset, collector
+ end
+
+ super
+ end
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "raw_rnum_ > "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Except(o, collector)
+ collector << "( "
+ collector = infix_value o, collector, " MINUS "
+ collector << " )"
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ # Oracle does not allow ORDER BY/LIMIT in UPDATEs.
+ if o.orders.any? && o.limit.nil?
+ # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided,
+ # otherwise let the user deal with the error
+ o = o.dup
+ o.orders = []
+ end
+
+ super
+ end
+
+ ###
+ # Hacks for the order clauses specific to Oracle
+ def order_hacks(o)
+ return o if o.orders.empty?
+ return o unless o.cores.any? do |core|
+ core.projections.any? do |projection|
+ /FIRST_VALUE/ === projection
+ end
+ end
+ # Previous version with join and split broke ORDER BY clause
+ # if it contained functions with several arguments (separated by ',').
+ #
+ # orders = o.orders.map { |x| visit x }.join(', ').split(',')
+ orders = o.orders.map do |x|
+ string = visit(x, Arel::Collectors::SQLString.new).value
+ if string.include?(",")
+ split_order_string(string)
+ else
+ string
+ end
+ end.flatten
+ o.orders = []
+ orders.each_with_index do |order, i|
+ o.orders <<
+ Nodes::SqlLiteral.new("alias_#{i}__#{' DESC' if /\bdesc$/i === order}")
+ end
+ o
+ end
+
+ # Split string by commas but count opening and closing brackets
+ # and ignore commas inside brackets.
+ def split_order_string(string)
+ array = []
+ i = 0
+ string.split(",").each do |part|
+ if array[i]
+ array[i] << "," << part
+ else
+ # to ensure that array[i] will be String and not Arel::Nodes::SqlLiteral
+ array[i] = part.to_s
+ end
+ i += 1 if array[i].count("(") == array[i].count(")")
+ end
+ array
+ end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { |i| ":a#{i}" }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb
new file mode 100644
index 0000000000..7061f06087
--- /dev/null
+++ b/activerecord/lib/arel/visitors/oracle12.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Oracle12 < Arel::Visitors::ToSql
+ private
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ # Oracle does not allow LIMIT clause with select for update
+ if o.limit && o.lock
+ raise ArgumentError, <<-MSG
+ 'Combination of limit and lock is not supported.
+ because generated SQL statements
+ `SELECT FOR UPDATE and FETCH FIRST n ROWS` generates ORA-02014.`
+ MSG
+ end
+ super
+ end
+
+ def visit_Arel_Nodes_SelectOptions(o, collector)
+ collector = maybe_visit o.offset, collector
+ collector = maybe_visit o.limit, collector
+ collector = maybe_visit o.lock, collector
+ end
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "FETCH FIRST "
+ collector = visit o.expr, collector
+ collector << " ROWS ONLY"
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "OFFSET "
+ visit o.expr, collector
+ collector << " ROWS"
+ end
+
+ def visit_Arel_Nodes_Except(o, collector)
+ collector << "( "
+ collector = infix_value o, collector, " MINUS "
+ collector << " )"
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ # Oracle does not allow ORDER BY/LIMIT in UPDATEs.
+ if o.orders.any? && o.limit.nil?
+ # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided,
+ # otherwise let the user deal with the error
+ o = o.dup
+ o.orders = []
+ end
+
+ super
+ end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { |i| ":a#{i}" }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb
new file mode 100644
index 0000000000..108ee431ee
--- /dev/null
+++ b/activerecord/lib/arel/visitors/postgresql.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class PostgreSQL < Arel::Visitors::ToSql
+ CUBE = "CUBE"
+ ROLLUP = "ROLLUP"
+ GROUPING_SET = "GROUPING SET"
+ LATERAL = "LATERAL"
+
+ private
+
+ def visit_Arel_Nodes_Matches(o, collector)
+ op = o.case_sensitive ? " LIKE " : " ILIKE "
+ collector = infix_value o, collector, op
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_DoesNotMatch(o, collector)
+ op = o.case_sensitive ? " NOT LIKE " : " NOT ILIKE "
+ collector = infix_value o, collector, op
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Regexp(o, collector)
+ op = o.case_sensitive ? " ~ " : " ~* "
+ infix_value o, collector, op
+ end
+
+ def visit_Arel_Nodes_NotRegexp(o, collector)
+ op = o.case_sensitive ? " !~ " : " !~* "
+ infix_value o, collector, op
+ end
+
+ def visit_Arel_Nodes_DistinctOn(o, collector)
+ collector << "DISTINCT ON ( "
+ visit(o.expr, collector) << " )"
+ end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { |i| "$#{i}" }
+ end
+
+ def visit_Arel_Nodes_GroupingElement(o, collector)
+ collector << "( "
+ visit(o.expr, collector) << " )"
+ end
+
+ def visit_Arel_Nodes_Cube(o, collector)
+ collector << CUBE
+ grouping_array_or_grouping_element o, collector
+ end
+
+ def visit_Arel_Nodes_RollUp(o, collector)
+ collector << ROLLUP
+ grouping_array_or_grouping_element o, collector
+ end
+
+ def visit_Arel_Nodes_GroupingSet(o, collector)
+ collector << GROUPING_SET
+ grouping_array_or_grouping_element o, collector
+ end
+
+ def visit_Arel_Nodes_Lateral(o, collector)
+ collector << LATERAL
+ collector << SPACE
+ grouping_parentheses o, collector
+ end
+
+ # Used by Lateral visitor to enclose select queries in parentheses
+ def grouping_parentheses(o, collector)
+ if o.expr.is_a? Nodes::SelectStatement
+ collector << "("
+ visit o.expr, collector
+ collector << ")"
+ else
+ visit o.expr, collector
+ end
+ end
+
+ # Utilized by GroupingSet, Cube & RollUp visitors to
+ # handle grouping aggregation semantics
+ def grouping_array_or_grouping_element(o, collector)
+ if o.expr.is_a? Array
+ collector << "( "
+ visit o.expr, collector
+ collector << " )"
+ else
+ visit o.expr, collector
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/sqlite.rb b/activerecord/lib/arel/visitors/sqlite.rb
new file mode 100644
index 0000000000..cb1d2424ad
--- /dev/null
+++ b/activerecord/lib/arel/visitors/sqlite.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class SQLite < Arel::Visitors::ToSql
+ private
+
+ # Locks are not supported in SQLite
+ def visit_Arel_Nodes_Lock(o, collector)
+ collector
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ o.limit = Arel::Nodes::Limit.new(-1) if o.offset && !o.limit
+ super
+ end
+
+ def visit_Arel_Nodes_True(o, collector)
+ collector << "1"
+ end
+
+ def visit_Arel_Nodes_False(o, collector)
+ collector << "0"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb
new file mode 100644
index 0000000000..5986fd5576
--- /dev/null
+++ b/activerecord/lib/arel/visitors/to_sql.rb
@@ -0,0 +1,847 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class UnsupportedVisitError < StandardError
+ def initialize(object)
+ super "Unsupported argument type: #{object.class.name}. Construct an Arel node instead."
+ end
+ end
+
+ class ToSql < Arel::Visitors::Visitor
+ ##
+ # This is some roflscale crazy stuff. I'm roflscaling this because
+ # building SQL queries is a hotspot. I will explain the roflscale so that
+ # others will not rm this code.
+ #
+ # In YARV, string literals in a method body will get duped when the byte
+ # code is executed. Let's take a look:
+ #
+ # > puts RubyVM::InstructionSequence.new('def foo; "bar"; end').disasm
+ #
+ # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>=====
+ # 0000 trace 8
+ # 0002 trace 1
+ # 0004 putstring "bar"
+ # 0006 trace 16
+ # 0008 leave
+ #
+ # The `putstring` bytecode will dup the string and push it on the stack.
+ # In many cases in our SQL visitor, that string is never mutated, so there
+ # is no need to dup the literal.
+ #
+ # If we change to a constant lookup, the string will not be duped, and we
+ # can reduce the objects in our system:
+ #
+ # > puts RubyVM::InstructionSequence.new('BAR = "bar"; def foo; BAR; end').disasm
+ #
+ # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>========
+ # 0000 trace 8
+ # 0002 trace 1
+ # 0004 getinlinecache 11, <ic:0>
+ # 0007 getconstant :BAR
+ # 0009 setinlinecache <ic:0>
+ # 0011 trace 16
+ # 0013 leave
+ #
+ # `getconstant` should be a hash lookup, and no object is duped when the
+ # value of the constant is pushed on the stack. Hence the crazy
+ # constants below.
+ #
+ # `matches` and `doesNotMatch` operate case-insensitively via Visitor subclasses
+ # specialized for specific databases when necessary.
+ #
+
+ WHERE = " WHERE " # :nodoc:
+ SPACE = " " # :nodoc:
+ COMMA = ", " # :nodoc:
+ GROUP_BY = " GROUP BY " # :nodoc:
+ ORDER_BY = " ORDER BY " # :nodoc:
+ WINDOW = " WINDOW " # :nodoc:
+ AND = " AND " # :nodoc:
+
+ DISTINCT = "DISTINCT" # :nodoc:
+
+ def initialize(connection)
+ super()
+ @connection = connection
+ end
+
+ def compile(node, &block)
+ accept(node, Arel::Collectors::SQLString.new, &block).value
+ end
+
+ private
+
+ def visit_Arel_Nodes_DeleteStatement(o, collector)
+ collector << "DELETE FROM "
+ collector = visit o.relation, collector
+ if o.wheres.any?
+ collector << WHERE
+ collector = inject_join o.wheres, collector, AND
+ end
+
+ maybe_visit o.limit, collector
+ end
+
+ # FIXME: we should probably have a 2-pass visitor for this
+ def build_subselect(key, o)
+ stmt = Nodes::SelectStatement.new
+ core = stmt.cores.first
+ core.froms = o.relation
+ core.wheres = o.wheres
+ core.projections = [key]
+ stmt.limit = o.limit
+ stmt.orders = o.orders
+ stmt
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ if o.orders.empty? && o.limit.nil?
+ wheres = o.wheres
+ else
+ wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])]
+ end
+
+ collector << "UPDATE "
+ collector = visit o.relation, collector
+ unless o.values.empty?
+ collector << " SET "
+ collector = inject_join o.values, collector, ", "
+ end
+
+ unless wheres.empty?
+ collector << " WHERE "
+ collector = inject_join wheres, collector, " AND "
+ end
+
+ collector
+ end
+
+ def visit_Arel_Nodes_InsertStatement(o, collector)
+ collector << "INSERT INTO "
+ collector = visit o.relation, collector
+ if o.columns.any?
+ collector << " (#{o.columns.map { |x|
+ quote_column_name x.name
+ }.join ', '})"
+ end
+
+ if o.values
+ maybe_visit o.values, collector
+ elsif o.select
+ maybe_visit o.select, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Exists(o, collector)
+ collector << "EXISTS ("
+ collector = visit(o.expressions, collector) << ")"
+ if o.alias
+ collector << " AS "
+ visit o.alias, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Casted(o, collector)
+ collector << quoted(o.val, o.attribute).to_s
+ end
+
+ def visit_Arel_Nodes_Quoted(o, collector)
+ collector << quoted(o.expr, nil).to_s
+ end
+
+ def visit_Arel_Nodes_True(o, collector)
+ collector << "TRUE"
+ end
+
+ def visit_Arel_Nodes_False(o, collector)
+ collector << "FALSE"
+ end
+
+ def visit_Arel_Nodes_ValuesList(o, collector)
+ collector << "VALUES "
+
+ len = o.rows.length - 1
+ o.rows.each_with_index { |row, i|
+ collector << "("
+ row_len = row.length - 1
+ row.each_with_index do |value, k|
+ case value
+ when Nodes::SqlLiteral, Nodes::BindParam
+ collector = visit(value, collector)
+ else
+ collector << quote(value)
+ end
+ collector << COMMA unless k == row_len
+ end
+ collector << ")"
+ collector << COMMA unless i == len
+ }
+ collector
+ end
+
+ def visit_Arel_Nodes_Values(o, collector)
+ collector << "VALUES ("
+
+ len = o.expressions.length - 1
+ o.expressions.each_with_index { |value, i|
+ case value
+ when Nodes::SqlLiteral, Nodes::BindParam
+ collector = visit value, collector
+ else
+ collector << quote(value).to_s
+ end
+ unless i == len
+ collector << COMMA
+ end
+ }
+
+ collector << ")"
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ if o.with
+ collector = visit o.with, collector
+ collector << SPACE
+ end
+
+ collector = o.cores.inject(collector) { |c, x|
+ visit_Arel_Nodes_SelectCore(x, c)
+ }
+
+ unless o.orders.empty?
+ collector << ORDER_BY
+ len = o.orders.length - 1
+ o.orders.each_with_index { |x, i|
+ collector = visit(x, collector)
+ collector << COMMA unless len == i
+ }
+ end
+
+ visit_Arel_Nodes_SelectOptions(o, collector)
+
+ collector
+ end
+
+ def visit_Arel_Nodes_SelectOptions(o, collector)
+ collector = maybe_visit o.limit, collector
+ collector = maybe_visit o.offset, collector
+ collector = maybe_visit o.lock, collector
+ end
+
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector << "SELECT"
+
+ collector = maybe_visit o.top, collector
+
+ collector = maybe_visit o.set_quantifier, collector
+
+ collect_nodes_for o.projections, collector, SPACE
+
+ if o.source && !o.source.empty?
+ collector << " FROM "
+ collector = visit o.source, collector
+ end
+
+ collect_nodes_for o.wheres, collector, WHERE, AND
+ collect_nodes_for o.groups, collector, GROUP_BY
+ unless o.havings.empty?
+ collector << " HAVING "
+ inject_join o.havings, collector, AND
+ end
+ collect_nodes_for o.windows, collector, WINDOW
+
+ collector
+ end
+
+ def collect_nodes_for(nodes, collector, spacer, connector = COMMA)
+ unless nodes.empty?
+ collector << spacer
+ len = nodes.length - 1
+ nodes.each_with_index do |x, i|
+ collector = visit(x, collector)
+ collector << connector unless len == i
+ end
+ end
+ end
+
+ def visit_Arel_Nodes_Bin(o, collector)
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Distinct(o, collector)
+ collector << DISTINCT
+ end
+
+ def visit_Arel_Nodes_DistinctOn(o, collector)
+ raise NotImplementedError, "DISTINCT ON not implemented for this db"
+ end
+
+ def visit_Arel_Nodes_With(o, collector)
+ collector << "WITH "
+ inject_join o.children, collector, COMMA
+ end
+
+ def visit_Arel_Nodes_WithRecursive(o, collector)
+ collector << "WITH RECURSIVE "
+ inject_join o.children, collector, COMMA
+ end
+
+ def visit_Arel_Nodes_Union(o, collector)
+ collector << "( "
+ infix_value(o, collector, " UNION ") << " )"
+ end
+
+ def visit_Arel_Nodes_UnionAll(o, collector)
+ collector << "( "
+ infix_value(o, collector, " UNION ALL ") << " )"
+ end
+
+ def visit_Arel_Nodes_Intersect(o, collector)
+ collector << "( "
+ infix_value(o, collector, " INTERSECT ") << " )"
+ end
+
+ def visit_Arel_Nodes_Except(o, collector)
+ collector << "( "
+ infix_value(o, collector, " EXCEPT ") << " )"
+ end
+
+ def visit_Arel_Nodes_NamedWindow(o, collector)
+ collector << quote_column_name(o.name)
+ collector << " AS "
+ visit_Arel_Nodes_Window o, collector
+ end
+
+ def visit_Arel_Nodes_Window(o, collector)
+ collector << "("
+
+ if o.partitions.any?
+ collector << "PARTITION BY "
+ collector = inject_join o.partitions, collector, ", "
+ end
+
+ if o.orders.any?
+ collector << SPACE if o.partitions.any?
+ collector << "ORDER BY "
+ collector = inject_join o.orders, collector, ", "
+ end
+
+ if o.framing
+ collector << SPACE if o.partitions.any? || o.orders.any?
+ collector = visit o.framing, collector
+ end
+
+ collector << ")"
+ end
+
+ def visit_Arel_Nodes_Rows(o, collector)
+ if o.expr
+ collector << "ROWS "
+ visit o.expr, collector
+ else
+ collector << "ROWS"
+ end
+ end
+
+ def visit_Arel_Nodes_Range(o, collector)
+ if o.expr
+ collector << "RANGE "
+ visit o.expr, collector
+ else
+ collector << "RANGE"
+ end
+ end
+
+ def visit_Arel_Nodes_Preceding(o, collector)
+ collector = if o.expr
+ visit o.expr, collector
+ else
+ collector << "UNBOUNDED"
+ end
+
+ collector << " PRECEDING"
+ end
+
+ def visit_Arel_Nodes_Following(o, collector)
+ collector = if o.expr
+ visit o.expr, collector
+ else
+ collector << "UNBOUNDED"
+ end
+
+ collector << " FOLLOWING"
+ end
+
+ def visit_Arel_Nodes_CurrentRow(o, collector)
+ collector << "CURRENT ROW"
+ end
+
+ def visit_Arel_Nodes_Over(o, collector)
+ case o.right
+ when nil
+ visit(o.left, collector) << " OVER ()"
+ when Arel::Nodes::SqlLiteral
+ infix_value o, collector, " OVER "
+ when String, Symbol
+ visit(o.left, collector) << " OVER #{quote_column_name o.right.to_s}"
+ else
+ infix_value o, collector, " OVER "
+ end
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "OFFSET "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "LIMIT "
+ visit o.expr, collector
+ end
+
+ # FIXME: this does nothing on most databases, but does on MSSQL
+ def visit_Arel_Nodes_Top(o, collector)
+ collector
+ end
+
+ def visit_Arel_Nodes_Lock(o, collector)
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Grouping(o, collector)
+ if o.expr.is_a? Nodes::Grouping
+ visit(o.expr, collector)
+ else
+ collector << "("
+ visit(o.expr, collector) << ")"
+ end
+ end
+
+ def visit_Arel_SelectManager(o, collector)
+ collector << "("
+ visit(o.ast, collector) << ")"
+ end
+
+ def visit_Arel_Nodes_Ascending(o, collector)
+ visit(o.expr, collector) << " ASC"
+ end
+
+ def visit_Arel_Nodes_Descending(o, collector)
+ visit(o.expr, collector) << " DESC"
+ end
+
+ def visit_Arel_Nodes_Group(o, collector)
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_NamedFunction(o, collector)
+ collector << o.name
+ collector << "("
+ collector << "DISTINCT " if o.distinct
+ collector = inject_join(o.expressions, collector, ", ") << ")"
+ if o.alias
+ collector << " AS "
+ visit o.alias, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Extract(o, collector)
+ collector << "EXTRACT(#{o.field.to_s.upcase} FROM "
+ visit(o.expr, collector) << ")"
+ end
+
+ def visit_Arel_Nodes_Count(o, collector)
+ aggregate "COUNT", o, collector
+ end
+
+ def visit_Arel_Nodes_Sum(o, collector)
+ aggregate "SUM", o, collector
+ end
+
+ def visit_Arel_Nodes_Max(o, collector)
+ aggregate "MAX", o, collector
+ end
+
+ def visit_Arel_Nodes_Min(o, collector)
+ aggregate "MIN", o, collector
+ end
+
+ def visit_Arel_Nodes_Avg(o, collector)
+ aggregate "AVG", o, collector
+ end
+
+ def visit_Arel_Nodes_TableAlias(o, collector)
+ collector = visit o.relation, collector
+ collector << " "
+ collector << quote_table_name(o.name)
+ end
+
+ def visit_Arel_Nodes_Between(o, collector)
+ collector = visit o.left, collector
+ collector << " BETWEEN "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_GreaterThanOrEqual(o, collector)
+ collector = visit o.left, collector
+ collector << " >= "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_GreaterThan(o, collector)
+ collector = visit o.left, collector
+ collector << " > "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_LessThanOrEqual(o, collector)
+ collector = visit o.left, collector
+ collector << " <= "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_LessThan(o, collector)
+ collector = visit o.left, collector
+ collector << " < "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Matches(o, collector)
+ collector = visit o.left, collector
+ collector << " LIKE "
+ collector = visit o.right, collector
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_DoesNotMatch(o, collector)
+ collector = visit o.left, collector
+ collector << " NOT LIKE "
+ collector = visit o.right, collector
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_JoinSource(o, collector)
+ if o.left
+ collector = visit o.left, collector
+ end
+ if o.right.any?
+ collector << SPACE if o.left
+ collector = inject_join o.right, collector, SPACE
+ end
+ collector
+ end
+
+ def visit_Arel_Nodes_Regexp(o, collector)
+ raise NotImplementedError, "~ not implemented for this db"
+ end
+
+ def visit_Arel_Nodes_NotRegexp(o, collector)
+ raise NotImplementedError, "!~ not implemented for this db"
+ end
+
+ def visit_Arel_Nodes_StringJoin(o, collector)
+ visit o.left, collector
+ end
+
+ def visit_Arel_Nodes_FullOuterJoin(o, collector)
+ collector << "FULL OUTER JOIN "
+ collector = visit o.left, collector
+ collector << SPACE
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_OuterJoin(o, collector)
+ collector << "LEFT OUTER JOIN "
+ collector = visit o.left, collector
+ collector << " "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_RightOuterJoin(o, collector)
+ collector << "RIGHT OUTER JOIN "
+ collector = visit o.left, collector
+ collector << SPACE
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_InnerJoin(o, collector)
+ collector << "INNER JOIN "
+ collector = visit o.left, collector
+ if o.right
+ collector << SPACE
+ visit(o.right, collector)
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_On(o, collector)
+ collector << "ON "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Not(o, collector)
+ collector << "NOT ("
+ visit(o.expr, collector) << ")"
+ end
+
+ def visit_Arel_Table(o, collector)
+ if o.table_alias
+ collector << "#{quote_table_name o.name} #{quote_table_name o.table_alias}"
+ else
+ collector << quote_table_name(o.name)
+ end
+ end
+
+ def visit_Arel_Nodes_In(o, collector)
+ if Array === o.right && o.right.empty?
+ collector << "1=0"
+ else
+ collector = visit o.left, collector
+ collector << " IN ("
+ visit(o.right, collector) << ")"
+ end
+ end
+
+ def visit_Arel_Nodes_NotIn(o, collector)
+ if Array === o.right && o.right.empty?
+ collector << "1=1"
+ else
+ collector = visit o.left, collector
+ collector << " NOT IN ("
+ collector = visit o.right, collector
+ collector << ")"
+ end
+ end
+
+ def visit_Arel_Nodes_And(o, collector)
+ inject_join o.children, collector, " AND "
+ end
+
+ def visit_Arel_Nodes_Or(o, collector)
+ collector = visit o.left, collector
+ collector << " OR "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Assignment(o, collector)
+ case o.right
+ when Arel::Nodes::UnqualifiedColumn, Arel::Attributes::Attribute, Arel::Nodes::BindParam
+ collector = visit o.left, collector
+ collector << " = "
+ visit o.right, collector
+ else
+ collector = visit o.left, collector
+ collector << " = "
+ collector << quote(o.right).to_s
+ end
+ end
+
+ def visit_Arel_Nodes_Equality(o, collector)
+ right = o.right
+
+ collector = visit o.left, collector
+
+ if right.nil?
+ collector << " IS NULL"
+ else
+ collector << " = "
+ visit right, collector
+ end
+ end
+
+ def visit_Arel_Nodes_NotEqual(o, collector)
+ right = o.right
+
+ collector = visit o.left, collector
+
+ if right.nil?
+ collector << " IS NOT NULL"
+ else
+ collector << " != "
+ visit right, collector
+ end
+ end
+
+ def visit_Arel_Nodes_As(o, collector)
+ collector = visit o.left, collector
+ collector << " AS "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Case(o, collector)
+ collector << "CASE "
+ if o.case
+ visit o.case, collector
+ collector << " "
+ end
+ o.conditions.each do |condition|
+ visit condition, collector
+ collector << " "
+ end
+ if o.default
+ visit o.default, collector
+ collector << " "
+ end
+ collector << "END"
+ end
+
+ def visit_Arel_Nodes_When(o, collector)
+ collector << "WHEN "
+ visit o.left, collector
+ collector << " THEN "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Else(o, collector)
+ collector << "ELSE "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_UnqualifiedColumn(o, collector)
+ collector << "#{quote_column_name o.name}"
+ collector
+ end
+
+ def visit_Arel_Attributes_Attribute(o, collector)
+ join_name = o.relation.table_alias || o.relation.name
+ collector << "#{quote_table_name join_name}.#{quote_column_name o.name}"
+ end
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute
+
+ def literal(o, collector); collector << o.to_s; end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { "?" }
+ end
+
+ alias :visit_Arel_Nodes_SqlLiteral :literal
+ alias :visit_Bignum :literal
+ alias :visit_Fixnum :literal
+ alias :visit_Integer :literal
+
+ def quoted(o, a)
+ if a && a.able_to_type_cast?
+ quote(a.type_cast_for_database(o))
+ else
+ quote(o)
+ end
+ end
+
+ def unsupported(o, collector)
+ raise UnsupportedVisitError.new(o)
+ end
+
+ alias :visit_ActiveSupport_Multibyte_Chars :unsupported
+ alias :visit_ActiveSupport_StringInquirer :unsupported
+ alias :visit_BigDecimal :unsupported
+ alias :visit_Class :unsupported
+ alias :visit_Date :unsupported
+ alias :visit_DateTime :unsupported
+ alias :visit_FalseClass :unsupported
+ alias :visit_Float :unsupported
+ alias :visit_Hash :unsupported
+ alias :visit_NilClass :unsupported
+ alias :visit_String :unsupported
+ alias :visit_Symbol :unsupported
+ alias :visit_Time :unsupported
+ alias :visit_TrueClass :unsupported
+
+ def visit_Arel_Nodes_InfixOperation(o, collector)
+ collector = visit o.left, collector
+ collector << " #{o.operator} "
+ visit o.right, collector
+ end
+
+ alias :visit_Arel_Nodes_Addition :visit_Arel_Nodes_InfixOperation
+ alias :visit_Arel_Nodes_Subtraction :visit_Arel_Nodes_InfixOperation
+ alias :visit_Arel_Nodes_Multiplication :visit_Arel_Nodes_InfixOperation
+ alias :visit_Arel_Nodes_Division :visit_Arel_Nodes_InfixOperation
+
+ def visit_Arel_Nodes_UnaryOperation(o, collector)
+ collector << " #{o.operator} "
+ visit o.expr, collector
+ end
+
+ def visit_Array(o, collector)
+ inject_join o, collector, ", "
+ end
+ alias :visit_Set :visit_Array
+
+ def quote(value)
+ return value if Arel::Nodes::SqlLiteral === value
+ @connection.quote value
+ end
+
+ def quote_table_name(name)
+ return name if Arel::Nodes::SqlLiteral === name
+ @connection.quote_table_name(name)
+ end
+
+ def quote_column_name(name)
+ return name if Arel::Nodes::SqlLiteral === name
+ @connection.quote_column_name(name)
+ end
+
+ def maybe_visit(thing, collector)
+ return collector unless thing
+ collector << " "
+ visit thing, collector
+ end
+
+ def inject_join(list, collector, join_str)
+ len = list.length - 1
+ list.each_with_index.inject(collector) { |c, (x, i)|
+ if i == len
+ visit x, c
+ else
+ visit(x, c) << join_str
+ end
+ }
+ end
+
+ def infix_value(o, collector, value)
+ collector = visit o.left, collector
+ collector << value
+ visit o.right, collector
+ end
+
+ def aggregate(name, o, collector)
+ collector << "#{name}("
+ if o.distinct
+ collector << "DISTINCT "
+ end
+ collector = inject_join(o.expressions, collector, ", ") << ")"
+ if o.alias
+ collector << " AS "
+ visit o.alias, collector
+ else
+ collector
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/visitor.rb b/activerecord/lib/arel/visitors/visitor.rb
new file mode 100644
index 0000000000..1c17184e86
--- /dev/null
+++ b/activerecord/lib/arel/visitors/visitor.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Visitor
+ def initialize
+ @dispatch = get_dispatch_cache
+ end
+
+ def accept(object, *args)
+ visit object, *args
+ end
+
+ private
+
+ attr_reader :dispatch
+
+ def self.dispatch_cache
+ Hash.new do |hash, klass|
+ hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}"
+ end
+ end
+
+ def get_dispatch_cache
+ self.class.dispatch_cache
+ end
+
+ def visit(object, *args)
+ dispatch_method = dispatch[object.class]
+ send dispatch_method, object, *args
+ rescue NoMethodError => e
+ raise e if respond_to?(dispatch_method, true)
+ superklass = object.class.ancestors.find { |klass|
+ respond_to?(dispatch[klass], true)
+ }
+ raise(TypeError, "Cannot visit #{object.class}") unless superklass
+ dispatch[object.class] = dispatch[superklass]
+ retry
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/where_sql.rb b/activerecord/lib/arel/visitors/where_sql.rb
new file mode 100644
index 0000000000..c6caf5e7c9
--- /dev/null
+++ b/activerecord/lib/arel/visitors/where_sql.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class WhereSql < Arel::Visitors::ToSql
+ def initialize(inner_visitor, *args, &block)
+ @inner_visitor = inner_visitor
+ super(*args, &block)
+ end
+
+ private
+
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector << "WHERE "
+ wheres = o.wheres.map do |where|
+ Nodes::SqlLiteral.new(@inner_visitor.accept(where, collector.class.new).value)
+ end
+
+ inject_join wheres, collector, " AND "
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/window_predications.rb b/activerecord/lib/arel/window_predications.rb
new file mode 100644
index 0000000000..3a8ee41f8a
--- /dev/null
+++ b/activerecord/lib/arel/window_predications.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module WindowPredications
+ def over(expr = nil)
+ Nodes::Over.new(self, expr)
+ end
+ end
+end
diff --git a/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt
index 60050e0bf8..60050e0bf8 100644
--- a/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb
+++ b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt
diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
index 856fcc5897..a07b00ef79 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -15,12 +15,8 @@ module ActiveRecord
migration_template @migration_template, File.join(db_migrate_path, "#{file_name}.rb")
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
- attr_reader :migration_action, :join_tables
-
private
+ attr_reader :migration_action, :join_tables
# Sets the default migration template that is being used for the generation of the migration.
# Depending on command line arguments, the migration template and the table name instance
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt
index 5f7201cfe1..5f7201cfe1 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
index 481c70201b..481c70201b 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
index 55dc65c8ad..55dc65c8ad 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb
+++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/module.rb b/activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt
index a3bf1c37b6..a3bf1c37b6 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/module.rb
+++ b/activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt
diff --git a/activerecord/test/.gitignore b/activerecord/test/.gitignore
deleted file mode 100644
index a0ec5967dd..0000000000
--- a/activerecord/test/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/config.yml
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 9aaa2852d0..02c7e47583 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -20,7 +20,7 @@ module ActiveRecord
b = Book.create(name: "my \x00 book")
b.reload
assert_equal "my \x00 book", b.name
- b.update_attributes(name: "my other \x00 book")
+ b.update(name: "my other \x00 book")
b.reload
assert_equal "my other \x00 book", b.name
end
@@ -78,13 +78,13 @@ module ActiveRecord
idx_name = "accounts_idx"
indexes = @connection.indexes("accounts")
- assert indexes.empty?
+ assert_empty indexes
@connection.add_index :accounts, :firm_id, name: idx_name
indexes = @connection.indexes("accounts")
assert_equal "accounts", indexes.first.table
assert_equal idx_name, indexes.first.name
- assert !indexes.first.unique
+ assert_not indexes.first.unique
assert_equal ["firm_id"], indexes.first.columns
ensure
@connection.remove_index(:accounts, name: idx_name) rescue nil
@@ -368,16 +368,16 @@ module ActiveRecord
unless in_memory_db?
test "transaction state is reset after a reconnect" do
@connection.begin_transaction
- assert @connection.transaction_open?
+ assert_predicate @connection, :transaction_open?
@connection.reconnect!
- assert !@connection.transaction_open?
+ assert_not_predicate @connection, :transaction_open?
end
test "transaction state is reset after a disconnect" do
@connection.begin_transaction
- assert @connection.transaction_open?
+ assert_predicate @connection, :transaction_open?
@connection.disconnect!
- assert !@connection.transaction_open?
+ assert_not_predicate @connection, :transaction_open?
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index a6b83ec377..976c5dde58 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -9,7 +9,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
def setup
ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_without_stub, :execute
- def execute(sql, name = nil) return sql end
+ def execute(sql, name = nil) sql end
end
end
@@ -68,14 +68,14 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
def (ActiveRecord::Base.connection).data_source_exists?(*); false; end
%w(SPATIAL FULLTEXT UNIQUE).each do |type|
- expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`)) ENGINE=InnoDB"
+ expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`))"
actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t|
t.index :last_name, type: type
end
assert_equal expected, actual
end
- expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10))) ENGINE=InnoDB"
+ expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)))"
actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t|
t.index :last_name, length: 10, using: :btree
end
@@ -108,7 +108,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
def test_create_mysql_database_with_encoding
assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, charset: "latin1")
- assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, charset: :big5, collation: :big5_chinese_ci)
+ assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT COLLATE `utf8mb4_bin`", create_database(:matt_aimonetti, collation: "utf8mb4_bin")
end
def test_recreate_mysql_database_with_encoding
@@ -148,8 +148,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
t.timestamps null: true
end
ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true
- assert !column_present?("delete_me", "updated_at", "datetime")
- assert !column_present?("delete_me", "created_at", "datetime")
+ assert_not column_present?("delete_me", "updated_at", "datetime")
+ assert_not column_present?("delete_me", "created_at", "datetime")
ensure
ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
@@ -160,7 +160,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false)
ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
- expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
+ expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) AS SELECT id, name, zip FROM a_really_complicated_query"
actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
t.index :zip
end
diff --git a/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb b/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb
new file mode 100644
index 0000000000..4c67633946
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/schema_dumping_helper"
+
+class Mysql2AutoIncrementTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ @connection.drop_table :auto_increments, if_exists: true
+ end
+
+ def test_auto_increment_without_primary_key
+ @connection.create_table :auto_increments, id: false, force: true do |t|
+ t.integer :id, null: false, auto_increment: true
+ t.index :id
+ end
+ output = dump_table_schema("auto_increments")
+ assert_match(/t\.integer\s+"id",\s+null: false,\s+auto_increment: true$/, output)
+ end
+
+ def test_auto_increment_with_composite_primary_key
+ @connection.create_table :auto_increments, primary_key: [:id, :created_at], force: true do |t|
+ t.integer :id, null: false, auto_increment: true
+ t.datetime :created_at, null: false
+ end
+ output = dump_table_schema("auto_increments")
+ assert_match(/t\.integer\s+"id",\s+null: false,\s+auto_increment: true$/, output)
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
index fd5f712f1a..aa870349be 100644
--- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
@@ -14,8 +14,8 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase
end
def test_case_sensitive
- assert !CollationTest.columns_hash["string_ci_column"].case_sensitive?
- assert CollationTest.columns_hash["string_cs_column"].case_sensitive?
+ assert_not_predicate CollationTest.columns_hash["string_ci_column"], :case_sensitive?
+ assert_predicate CollationTest.columns_hash["string_cs_column"], :case_sensitive?
end
def test_case_insensitive_comparison_for_ci_column
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 9d81d506a0..726f58d58e 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -40,41 +40,29 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
end
def test_no_automatic_reconnection_after_timeout
- assert @connection.active?
+ assert_predicate @connection, :active?
@connection.update("set @@wait_timeout=1")
sleep 2
- assert !@connection.active?
+ assert_not_predicate @connection, :active?
ensure
# Repair all fixture connections so other tests won't break.
@fixture_connections.each(&:verify!)
end
def test_successful_reconnection_after_timeout_with_manual_reconnect
- assert @connection.active?
+ assert_predicate @connection, :active?
@connection.update("set @@wait_timeout=1")
sleep 2
@connection.reconnect!
- assert @connection.active?
+ assert_predicate @connection, :active?
end
def test_successful_reconnection_after_timeout_with_verify
- assert @connection.active?
+ assert_predicate @connection, :active?
@connection.update("set @@wait_timeout=1")
sleep 2
@connection.verify!
- assert @connection.active?
- end
-
- def test_verify_with_args_is_deprecated
- assert_deprecated do
- @connection.verify!(option: true)
- end
- assert_deprecated do
- @connection.verify!([])
- end
- assert_deprecated do
- @connection.verify!({})
- end
+ assert_predicate @connection, :active?
end
def test_execute_after_disconnect
@@ -186,10 +174,10 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
assert_equal "SCHEMA", @subscriber.logged[0][1]
end
- def test_logs_name_rename_column_sql
+ def test_logs_name_rename_column_for_alter
@connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))"
@subscriber.logged.clear
- @connection.send(:rename_column_sql, "bar_baz", "foo", "foo2")
+ @connection.send(:rename_column_for_alter, "bar_baz", "foo", "foo2")
assert_equal "SCHEMA", @subscriber.logged[0][1]
ensure
@connection.execute "DROP TABLE `bar_baz`"
diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb
index 108bec832c..832f5d61d1 100644
--- a/activerecord/test/cases/adapters/mysql2/enum_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb
@@ -13,11 +13,11 @@ class Mysql2EnumTest < ActiveRecord::Mysql2TestCase
def test_should_not_be_unsigned
column = EnumTest.columns_hash["enum_column"]
- assert_not column.unsigned?
+ assert_not_predicate column, :unsigned?
end
def test_should_not_be_bigint
column = EnumTest.columns_hash["enum_column"]
- assert_not column.bigint?
+ assert_not_predicate column, :bigint?
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb
index 2736f7cf0e..b8e778f0b0 100644
--- a/activerecord/test/cases/adapters/mysql2/explain_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb
@@ -1,23 +1,23 @@
# frozen_string_literal: true
require "cases/helper"
-require "models/developer"
-require "models/computer"
+require "models/author"
+require "models/post"
class Mysql2ExplainTest < ActiveRecord::Mysql2TestCase
- fixtures :developers
+ fixtures :authors
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
+ explain = Author.where(id: 1).explain
+ assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain
+ assert_match %r(authors |.* 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
+ explain = Author.where(id: 1).includes(:posts).explain
+ assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain
+ assert_match %r(authors |.* const), explain
+ assert_match %(EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`author_id` = 1), explain
+ assert_match %r(posts |.* ALL), explain
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 d18fb97e05..0719baaa23 100644
--- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
@@ -25,25 +25,25 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
end
def test_columns_for_distinct_one_order
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id", ["posts.created_at desc"])
end
def test_columns_for_distinct_few_orders
- assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
+ assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id",
@conn.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
end
def test_columns_for_distinct_with_case
assert_equal(
- "posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0",
+ "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id",
["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"])
)
end
def test_columns_for_distinct_blank_not_nil_orders
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "])
end
@@ -52,7 +52,7 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
def order.to_sql
"posts.created_at desc"
end
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id", [order])
end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
index 62abd694bb..d7d9a2d732 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -36,7 +36,7 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase
assert connection.column_exists?(table_name, :key, :string)
end
ensure
- ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
+ ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment
end
private
diff --git a/activerecord/test/cases/adapters/mysql2/table_options_test.rb b/activerecord/test/cases/adapters/mysql2/table_options_test.rb
index 9f6bd38a8c..1c92df940f 100644
--- a/activerecord/test/cases/adapters/mysql2/table_options_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb
@@ -42,3 +42,78 @@ class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase
assert_match %r{COLLATE=utf8mb4_bin}, options
end
end
+
+class Mysql2DefaultEngineOptionSchemaDumpTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false
+
+ def setup
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+ end
+
+ def teardown
+ ActiveRecord::Base.connection.drop_table "mysql_table_options", if_exists: true
+ ActiveRecord::Migration.verbose = @verbose_was
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ end
+
+ test "schema dump includes ENGINE=InnoDB if not provided" do
+ ActiveRecord::Base.connection.create_table "mysql_table_options", force: true
+
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{ENGINE=InnoDB}, options
+ end
+
+ test "schema dump includes ENGINE=InnoDB in legacy migrations" do
+ migration = Class.new(ActiveRecord::Migration[5.1]) do
+ def migrate(x)
+ create_table "mysql_table_options", force: true
+ end
+ end.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{ENGINE=InnoDB}, options
+ end
+end
+
+class Mysql2DefaultEngineOptionSqlOutputTest < ActiveRecord::Mysql2TestCase
+ self.use_transactional_tests = false
+
+ def setup
+ @logger_was = ActiveRecord::Base.logger
+ @log = StringIO.new
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(@log)
+ ActiveRecord::Migration.verbose = false
+ end
+
+ def teardown
+ ActiveRecord::Base.logger = @logger_was
+ ActiveRecord::Migration.verbose = @verbose_was
+ ActiveRecord::Base.connection.drop_table "mysql_table_options", if_exists: true
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ end
+
+ test "new migrations do not contain default ENGINE=InnoDB option" do
+ ActiveRecord::Base.connection.create_table "mysql_table_options", force: true
+
+ assert_no_match %r{ENGINE=InnoDB}, @log.string
+ end
+
+ test "legacy migrations contain default ENGINE=InnoDB option" do
+ migration = Class.new(ActiveRecord::Migration[5.1]) do
+ def migrate(x)
+ create_table "mysql_table_options", force: true
+ end
+ end.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert_match %r{ENGINE=InnoDB}, @log.string
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb
index 25d9f69a89..52e283f247 100644
--- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb
@@ -13,6 +13,7 @@ module ActiveRecord
setup do
@abort, Thread.abort_on_exception = Thread.abort_on_exception, false
+ Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception
@connection = ActiveRecord::Base.connection
@connection.clear_cache!
@@ -31,6 +32,7 @@ module ActiveRecord
@connection.drop_table "samples", if_exists: true
Thread.abort_on_exception = @abort
+ Thread.report_on_exception = @original_report_on_exception
end
test "raises Deadlocked when a deadlock is encountered" do
@@ -44,7 +46,7 @@ module ActiveRecord
Sample.transaction do
s1.lock!
barrier.wait
- s2.update_attributes value: 1
+ s2.update value: 1
end
end
@@ -52,7 +54,7 @@ module ActiveRecord
Sample.transaction do
s2.lock!
barrier.wait
- s1.update_attributes value: 2
+ s1.update value: 2
end
ensure
thread.join
@@ -60,9 +62,87 @@ module ActiveRecord
end
end
- test "raises TransactionTimeout when mysql raises ER_LOCK_WAIT_TIMEOUT" do
- assert_raises(ActiveRecord::TransactionTimeout) do
- ActiveRecord::Base.connection.execute("SIGNAL SQLSTATE 'HY000' SET MESSAGE_TEXT = 'Testing error', MYSQL_ERRNO = 1205;")
+ test "raises LockWaitTimeout when lock wait timeout exceeded" do
+ assert_raises(ActiveRecord::LockWaitTimeout) do
+ s = Sample.create!(value: 1)
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch1.count_down
+ latch2.wait
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch1.wait
+ Sample.connection.execute("SET innodb_lock_wait_timeout = 1")
+ Sample.lock.find(s.id)
+ end
+ ensure
+ Sample.connection.execute("SET innodb_lock_wait_timeout = DEFAULT")
+ latch2.count_down
+ thread.join
+ end
+ end
+ end
+
+ test "raises StatementTimeout when statement timeout exceeded" do
+ skip unless ActiveRecord::Base.connection.show_variable("max_execution_time")
+ assert_raises(ActiveRecord::StatementTimeout) do
+ s = Sample.create!(value: 1)
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch1.count_down
+ latch2.wait
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch1.wait
+ Sample.connection.execute("SET max_execution_time = 1")
+ Sample.lock.find(s.id)
+ end
+ ensure
+ Sample.connection.execute("SET max_execution_time = DEFAULT")
+ latch2.count_down
+ thread.join
+ end
+ end
+ end
+
+ test "raises QueryCanceled when canceling statement due to user request" do
+ assert_raises(ActiveRecord::QueryCanceled) do
+ s = Sample.create!(value: 1)
+ latch = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch.count_down
+ sleep(0.5)
+ conn = Sample.connection
+ pid = conn.query_value("SELECT id FROM information_schema.processlist WHERE info LIKE '% FOR UPDATE'")
+ conn.execute("KILL QUERY #{pid}")
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch.wait
+ Sample.lock.find(s.id)
+ end
+ ensure
+ thread.join
+ end
end
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 b01f5d7f5a..97da96003d 100644
--- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
@@ -54,7 +54,7 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
end
@connection.columns("unsigned_types").select { |c| /^unsigned_/.match?(c.name) }.each do |column|
- assert column.unsigned?
+ assert_predicate column, :unsigned?
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
index 9929237546..308ad1d854 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -59,6 +59,15 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
assert_equal expected, add_index(:people, "lower(last_name)", using: type, unique: true)
end
+ expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name" bpchar_pattern_ops))
+ assert_equal expected, add_index(:people, :last_name, using: :gist, opclass: { last_name: :bpchar_pattern_ops })
+
+ expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC NULLS LAST, "first_name" ASC))
+ assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: "DESC NULLS LAST", first_name: :asc })
+
+ expected = %(CREATE INDEX "index_people_on_last_name" ON "people" ("last_name" NULLS FIRST))
+ assert_equal expected, add_index(:people, :last_name, order: "NULLS FIRST")
+
assert_raise ArgumentError do
add_index(:people, :last_name, algorithm: :copy)
end
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 0e9e86f425..58fa7532a2 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -39,12 +39,12 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
def test_column
assert_equal :string, @column.type
assert_equal "character varying(255)", @column.sql_type
- assert @column.array?
- assert_not @type.binary?
+ assert_predicate @column, :array?
+ assert_not_predicate @type, :binary?
ratings_column = PgArray.columns_hash["ratings"]
assert_equal :integer, ratings_column.type
- assert ratings_column.array?
+ assert_predicate ratings_column, :array?
end
def test_not_compatible_with_serialize_array
@@ -109,7 +109,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
assert_equal :text, column.type
assert_equal [], PgArray.column_defaults["snippets"]
- assert column.array?
+ assert_predicate column, :array?
end
def test_change_column_cant_make_non_array_column_to_array
@@ -228,7 +228,9 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
def test_insert_fixtures
tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"]
- @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays")
+ assert_deprecated do
+ @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays")
+ end
assert_equal(PgArray.last.tags, tag_values)
end
@@ -255,7 +257,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
x = PgArray.create!(tags: tags)
x.reload
- refute x.changed?
+ assert_not_predicate x, :changed?
end
def test_quoting_non_standard_delimiters
@@ -277,7 +279,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
x.reload
assert_equal %w(one two three), x.tags
- assert_not x.changed?
+ assert_not_predicate x, :changed?
end
def test_mutate_value_in_array
@@ -288,7 +290,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
x.reload
assert_equal [{ "a" => "c" }, { "b" => "b" }], x.hstores
- assert_not x.changed?
+ assert_not_predicate x, :changed?
end
def test_datetime_with_timezone_awareness
diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
index df04299569..c8e728bbb6 100644
--- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
@@ -29,20 +29,20 @@ class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlBitString.columns_hash["a_bit"]
assert_equal :bit, column.type
assert_equal "bit(8)", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlBitString.type_for_attribute("a_bit")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_bit_string_varying_column
column = PostgresqlBitString.columns_hash["a_bit_varying"]
assert_equal :bit_varying, column.type
assert_equal "bit varying(4)", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlBitString.type_for_attribute("a_bit_varying")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_default
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
index a6bee113ff..64bb6906cd 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -75,7 +75,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
def test_write_value
data = "\u001F"
record = ByteaDataType.create(payload: data)
- assert_not record.new_record?
+ assert_not_predicate record, :new_record?
assert_equal(data, record.payload)
end
@@ -101,14 +101,14 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
data = File.read(File.join(__dir__, "..", "..", "..", "assets", "example.log"))
assert(data.size > 1)
record = ByteaDataType.create(payload: data)
- assert_not record.new_record?
+ assert_not_predicate record, :new_record?
assert_equal(data, record.payload)
assert_equal(data, ByteaDataType.where(id: record.id).first.payload)
end
def test_write_nil
record = ByteaDataType.create(payload: nil)
- assert_not record.new_record?
+ assert_not_predicate record, :new_record?
assert_nil(record.payload)
assert_nil(ByteaDataType.where(id: record.id).first.payload)
end
diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb
index adf461a9cc..6dba4f3e14 100644
--- a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb
@@ -33,7 +33,7 @@ module ActiveRecord
connection.change_column :strings, :somedate, :timestamp, array: true, cast_as: :timestamp
column = connection.columns(:strings).find { |c| c.name == "somedate" }
assert_equal :datetime, column.type
- assert column.array?
+ assert_predicate column, :array?
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb
index 050614cade..9eb0b7d99c 100644
--- a/activerecord/test/cases/adapters/postgresql/citext_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb
@@ -3,78 +3,76 @@
require "cases/helper"
require "support/schema_dumping_helper"
-if ActiveRecord::Base.connection.supports_extensions?
- class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase
- include SchemaDumpingHelper
- class Citext < ActiveRecord::Base
- self.table_name = "citexts"
- end
-
- def setup
- @connection = ActiveRecord::Base.connection
+class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+ class Citext < ActiveRecord::Base
+ self.table_name = "citexts"
+ end
- enable_extension!("citext", @connection)
+ def setup
+ @connection = ActiveRecord::Base.connection
- @connection.create_table("citexts") do |t|
- t.citext "cival"
- end
- end
+ enable_extension!("citext", @connection)
- teardown do
- @connection.drop_table "citexts", if_exists: true
- disable_extension!("citext", @connection)
+ @connection.create_table("citexts") do |t|
+ t.citext "cival"
end
+ end
- def test_citext_enabled
- assert @connection.extension_enabled?("citext")
- end
+ teardown do
+ @connection.drop_table "citexts", if_exists: true
+ disable_extension!("citext", @connection)
+ end
- def test_column
- column = Citext.columns_hash["cival"]
- assert_equal :citext, column.type
- assert_equal "citext", column.sql_type
- assert_not column.array?
+ def test_citext_enabled
+ assert @connection.extension_enabled?("citext")
+ end
- type = Citext.type_for_attribute("cival")
- assert_not type.binary?
- end
+ def test_column
+ column = Citext.columns_hash["cival"]
+ assert_equal :citext, column.type
+ assert_equal "citext", column.sql_type
+ assert_not_predicate column, :array?
- def test_change_table_supports_json
- @connection.transaction do
- @connection.change_table("citexts") do |t|
- t.citext "username"
- end
- Citext.reset_column_information
- column = Citext.columns_hash["username"]
- assert_equal :citext, column.type
+ type = Citext.type_for_attribute("cival")
+ assert_not_predicate type, :binary?
+ end
- raise ActiveRecord::Rollback # reset the schema change
+ def test_change_table_supports_json
+ @connection.transaction do
+ @connection.change_table("citexts") do |t|
+ t.citext "username"
end
- ensure
Citext.reset_column_information
+ column = Citext.columns_hash["username"]
+ assert_equal :citext, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
end
+ ensure
+ Citext.reset_column_information
+ end
- def test_write
- x = Citext.new(cival: "Some CI Text")
- x.save!
- citext = Citext.first
- assert_equal "Some CI Text", citext.cival
+ def test_write
+ x = Citext.new(cival: "Some CI Text")
+ x.save!
+ citext = Citext.first
+ assert_equal "Some CI Text", citext.cival
- citext.cival = "Some NEW CI Text"
- citext.save!
+ citext.cival = "Some NEW CI Text"
+ citext.save!
- assert_equal "Some NEW CI Text", citext.reload.cival
- end
+ assert_equal "Some NEW CI Text", citext.reload.cival
+ end
- def test_select_case_insensitive
- @connection.execute "insert into citexts (cival) values('Cased Text')"
- x = Citext.where(cival: "cased text").first
- assert_equal "Cased Text", x.cival
- end
+ def test_select_case_insensitive
+ @connection.execute "insert into citexts (cival) values('Cased Text')"
+ x = Citext.where(cival: "cased text").first
+ assert_equal "Cased Text", x.cival
+ end
- def test_schema_dump_with_shorthand
- output = dump_table_schema("citexts")
- assert_match %r[t\.citext "cival"], output
- end
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("citexts")
+ assert_match %r[t\.citext "cival"], output
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb
index 5da95f7e2c..b0ce2694a3 100644
--- a/activerecord/test/cases/adapters/postgresql/composite_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb
@@ -51,10 +51,10 @@ class PostgresqlCompositeTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlComposite.columns_hash["address"]
assert_nil column.type
assert_equal "full_address", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlComposite.type_for_attribute("address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_composite_mapping
@@ -113,10 +113,10 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlComposite.columns_hash["address"]
assert_equal :full_address, column.type
assert_equal "full_address", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlComposite.type_for_attribute("address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_composite_mapping
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 2bb217a8b1..d1b3c434e1 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -103,7 +103,7 @@ module ActiveRecord
end
def test_indexes_logs_name
- assert_deprecated { @connection.indexes("items", "hello") }
+ @connection.indexes("items")
assert_equal "SCHEMA", @subscriber.logged[0][1]
end
@@ -157,7 +157,7 @@ module ActiveRecord
original_connection_pid = @connection.query("select pg_backend_pid()")
# Sanity check.
- assert @connection.active?
+ assert_predicate @connection, :active?
if @connection.send(:postgresql_version) >= 90200
secondary_connection = ActiveRecord::Base.connection_pool.checkout
@@ -176,7 +176,7 @@ module ActiveRecord
@connection.verify!
- assert @connection.active?
+ assert_predicate @connection, :active?
# If we get no exception here, then either we re-connected successfully, or
# we never actually got disconnected.
diff --git a/activerecord/test/cases/adapters/postgresql/date_test.rb b/activerecord/test/cases/adapters/postgresql/date_test.rb
new file mode 100644
index 0000000000..a86abac2be
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/date_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+
+class PostgresqlDateTest < ActiveRecord::PostgreSQLTestCase
+ def test_load_infinity_and_beyond
+ topic = Topic.find_by_sql("SELECT 'infinity'::date AS last_read").first
+ assert topic.last_read.infinite?, "timestamp should be infinite"
+ assert_operator topic.last_read, :>, 0
+
+ topic = Topic.find_by_sql("SELECT '-infinity'::date AS last_read").first
+ assert topic.last_read.infinite?, "timestamp should be infinite"
+ assert_operator topic.last_read, :<, 0
+ end
+
+ def test_save_infinity_and_beyond
+ topic = Topic.create!(last_read: 1.0 / 0.0)
+ assert_equal(1.0 / 0.0, topic.last_read)
+
+ topic = Topic.create!(last_read: -1.0 / 0.0)
+ assert_equal(-1.0 / 0.0, topic.last_read)
+ end
+
+ def test_bc_date
+ date = Date.new(0) - 1.week
+ topic = Topic.create!(last_read: date)
+ assert_equal date, Topic.find(topic.id).last_read
+ end
+
+ def test_bc_date_leap_year
+ date = Time.utc(-4, 2, 29).to_date
+ topic = Topic.create!(last_read: date)
+ assert_equal date, Topic.find(topic.id).last_read
+ end
+
+ def test_bc_date_year_zero
+ date = Time.utc(0, 4, 7).to_date
+ topic = Topic.create!(last_read: date)
+ assert_equal date, Topic.find(topic.id).last_read
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb
index 9c3817e2ad..eeaad94c27 100644
--- a/activerecord/test/cases/adapters/postgresql/domain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb
@@ -30,10 +30,10 @@ class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlDomain.columns_hash["price"]
assert_equal :decimal, column.type
assert_equal "custom_money", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlDomain.type_for_attribute("price")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_domain_acts_like_basetype
@@ -44,6 +44,6 @@ class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase
record.price = "34.15"
record.save!
- assert_equal BigDecimal.new("34.15"), record.reload.price
+ assert_equal BigDecimal("34.15"), record.reload.price
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb
index 3d3cbe11a3..6789ff63e7 100644
--- a/activerecord/test/cases/adapters/postgresql/enum_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb
@@ -32,10 +32,10 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlEnum.columns_hash["current_mood"]
assert_equal :enum, column.type
assert_equal "mood", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlEnum.type_for_attribute("current_mood")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_enum_defaults
@@ -73,7 +73,7 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase
@connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');"
stderr_output = capture(:stderr) { PostgresqlEnum.first }
- assert stderr_output.blank?
+ assert_predicate stderr_output, :blank?
end
def test_enum_type_cast
diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb
index 16fec94ede..be525383e9 100644
--- a/activerecord/test/cases/adapters/postgresql/explain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb
@@ -1,22 +1,22 @@
# frozen_string_literal: true
require "cases/helper"
-require "models/developer"
-require "models/computer"
+require "models/author"
+require "models/post"
class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase
- fixtures :developers
+ fixtures :authors
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 \[\["id", 1\]\]|1)), explain
+ explain = Author.where(id: 1).explain
+ assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain
assert_match %(QUERY PLAN), explain
end
def test_explain_with_eager_loading
- explain = Developer.where(id: 1).includes(:audit_logs).explain
+ explain = Author.where(id: 1).includes(:posts).explain
assert_match %(QUERY PLAN), explain
- assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain
- assert_match %r(EXPLAIN for: SELECT "audit_logs"\.\* FROM "audit_logs" WHERE "audit_logs"\."developer_id" = (?:\$1 \[\["developer_id", 1\]\]|1)), explain
+ assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain
+ assert_match %r(EXPLAIN for: SELECT "posts"\.\* FROM "posts" WHERE "posts"\."author_id" = (?:\$1 \[\["author_id", 1\]\]|1)), explain
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
index e589e3ab1b..df97ab11e7 100644
--- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
@@ -22,10 +22,6 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase
@connection = ActiveRecord::Base.connection
- unless @connection.supports_extensions?
- return skip("no extension support")
- end
-
@old_schema_migration_table_name = ActiveRecord::SchemaMigration.table_name
@old_table_name_prefix = ActiveRecord::Base.table_name_prefix
@old_table_name_suffix = ActiveRecord::Base.table_name_suffix
diff --git a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb
new file mode 100644
index 0000000000..4fa315ad23
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/professor"
+
+if ActiveRecord::Base.connection.supports_foreign_tables?
+ class ForeignTableTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ class ForeignProfessor < ActiveRecord::Base
+ self.table_name = "foreign_professors"
+ end
+
+ class ForeignProfessorWithPk < ForeignProfessor
+ self.primary_key = "id"
+ end
+
+ def setup
+ @professor = Professor.create(name: "Nicola")
+
+ @connection = ActiveRecord::Base.connection
+ enable_extension!("postgres_fdw", @connection)
+
+ foreign_db_config = ARTest.connection_config["arunit2"]
+ @connection.execute <<-SQL
+ CREATE SERVER foreign_server
+ FOREIGN DATA WRAPPER postgres_fdw
+ OPTIONS (dbname '#{foreign_db_config["database"]}')
+ SQL
+
+ @connection.execute <<-SQL
+ CREATE USER MAPPING FOR CURRENT_USER
+ SERVER foreign_server
+ SQL
+
+ @connection.execute <<-SQL
+ CREATE FOREIGN TABLE foreign_professors (
+ id int,
+ name character varying NOT NULL
+ ) SERVER foreign_server OPTIONS (
+ table_name 'professors'
+ )
+ SQL
+ end
+
+ def teardown
+ disable_extension!("postgres_fdw", @connection)
+ @connection.execute <<-SQL
+ DROP SERVER IF EXISTS foreign_server CASCADE
+ SQL
+ end
+
+ def test_table_exists
+ table_name = ForeignProfessor.table_name
+ assert_not ActiveRecord::Base.connection.table_exists?(table_name)
+ end
+
+ def test_foreign_tables_are_valid_data_sources
+ table_name = ForeignProfessor.table_name
+ assert @connection.data_source_exists?(table_name), "'#{table_name}' should be a data source"
+ end
+
+ def test_foreign_tables
+ assert_equal ["foreign_professors"], @connection.foreign_tables
+ end
+
+ def test_foreign_table_exists
+ assert @connection.foreign_table_exists?("foreign_professors")
+ assert @connection.foreign_table_exists?(:foreign_professors)
+ assert_not @connection.foreign_table_exists?("nonexistingtable")
+ assert_not @connection.foreign_table_exists?("'")
+ assert_not @connection.foreign_table_exists?(nil)
+ end
+
+ def test_attribute_names
+ assert_equal ["id", "name"], ForeignProfessor.attribute_names
+ end
+
+ def test_attributes
+ professor = ForeignProfessorWithPk.find(@professor.id)
+ assert_equal @professor.attributes, professor.attributes
+ end
+
+ def test_does_not_have_a_primary_key
+ assert_nil ForeignProfessor.primary_key
+ end
+
+ def test_insert_record
+ # Explicit `id` here to avoid complex configurations to implicitly work with remote table
+ ForeignProfessorWithPk.create!(id: 100, name: "Leonardo")
+
+ professor = ForeignProfessorWithPk.last
+ assert_equal "Leonardo", professor.name
+ end
+
+ def test_update_record
+ professor = ForeignProfessorWithPk.find(@professor.id)
+ professor.name = "Albert"
+ professor.save!
+ professor.reload
+ assert_equal "Albert", professor.name
+ end
+
+ def test_delete_record
+ professor = ForeignProfessorWithPk.find(@professor.id)
+ assert_difference("ForeignProfessor.count", -1) { professor.destroy }
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
index c6f1e1727f..95dee3bf44 100644
--- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
@@ -22,10 +22,10 @@ class PostgresqlFullTextTest < ActiveRecord::PostgreSQLTestCase
column = Tsvector.columns_hash["text_vector"]
assert_equal :tsvector, column.type
assert_equal "tsvector", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = Tsvector.type_for_attribute("text_vector")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_update_tsvector
diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
index e1ba00e07b..8c6f046553 100644
--- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -39,10 +39,10 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlPoint.columns_hash["x"]
assert_equal :point, column.type
assert_equal "point", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlPoint.type_for_attribute("x")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_default
@@ -79,7 +79,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
p.reload
assert_equal ActiveRecord::Point.new(10.0, 25.0), p.x
- assert_not p.changed?
+ assert_not_predicate p, :changed?
end
def test_array_assignment
@@ -117,10 +117,10 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlPoint.columns_hash["legacy_x"]
assert_equal :point, column.type
assert_equal "point", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlPoint.type_for_attribute("legacy_x")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_legacy_default
@@ -157,7 +157,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
p.reload
assert_equal [10.0, 25.0], p.legacy_x
- assert_not p.changed?
+ assert_not_predicate p, :changed?
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index 97a8a257c5..4b061a9375 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -3,378 +3,376 @@
require "cases/helper"
require "support/schema_dumping_helper"
-if ActiveRecord::Base.connection.supports_extensions?
- class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase
- include SchemaDumpingHelper
- class Hstore < ActiveRecord::Base
- self.table_name = "hstores"
+class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+ class Hstore < ActiveRecord::Base
+ self.table_name = "hstores"
- store_accessor :settings, :language, :timezone
- end
+ store_accessor :settings, :language, :timezone
+ end
- class FakeParameters
- def to_unsafe_h
- { "hi" => "hi" }
- end
+ class FakeParameters
+ def to_unsafe_h
+ { "hi" => "hi" }
end
+ end
- def setup
- @connection = ActiveRecord::Base.connection
+ def setup
+ @connection = ActiveRecord::Base.connection
- enable_extension!("hstore", @connection)
+ enable_extension!("hstore", @connection)
- @connection.transaction do
- @connection.create_table("hstores") do |t|
- t.hstore "tags", default: ""
- t.hstore "payload", array: true
- t.hstore "settings"
- end
+ @connection.transaction do
+ @connection.create_table("hstores") do |t|
+ t.hstore "tags", default: ""
+ t.hstore "payload", array: true
+ t.hstore "settings"
end
- Hstore.reset_column_information
- @column = Hstore.columns_hash["tags"]
- @type = Hstore.type_for_attribute("tags")
- end
-
- teardown do
- @connection.drop_table "hstores", if_exists: true
- disable_extension!("hstore", @connection)
end
+ Hstore.reset_column_information
+ @column = Hstore.columns_hash["tags"]
+ @type = Hstore.type_for_attribute("tags")
+ end
- def test_hstore_included_in_extensions
- assert @connection.respond_to?(:extensions), "connection should have a list of extensions"
- assert_includes @connection.extensions, "hstore", "extension list should include hstore"
- end
+ teardown do
+ @connection.drop_table "hstores", if_exists: true
+ disable_extension!("hstore", @connection)
+ end
- def test_disable_enable_hstore
- assert @connection.extension_enabled?("hstore")
- @connection.disable_extension "hstore"
- assert_not @connection.extension_enabled?("hstore")
- @connection.enable_extension "hstore"
- assert @connection.extension_enabled?("hstore")
- ensure
- # Restore column(s) dropped by `drop extension hstore cascade;`
- load_schema
- end
+ def test_hstore_included_in_extensions
+ assert_respond_to @connection, :extensions
+ assert_includes @connection.extensions, "hstore", "extension list should include hstore"
+ end
- def test_column
- assert_equal :hstore, @column.type
- assert_equal "hstore", @column.sql_type
- assert_not @column.array?
+ def test_disable_enable_hstore
+ assert @connection.extension_enabled?("hstore")
+ @connection.disable_extension "hstore"
+ assert_not @connection.extension_enabled?("hstore")
+ @connection.enable_extension "hstore"
+ assert @connection.extension_enabled?("hstore")
+ ensure
+ # Restore column(s) dropped by `drop extension hstore cascade;`
+ load_schema
+ end
- assert_not @type.binary?
- end
+ def test_column
+ assert_equal :hstore, @column.type
+ assert_equal "hstore", @column.sql_type
+ assert_not_predicate @column, :array?
- def test_default
- @connection.add_column "hstores", "permissions", :hstore, default: '"users"=>"read", "articles"=>"write"'
- Hstore.reset_column_information
+ assert_not_predicate @type, :binary?
+ end
- assert_equal({ "users" => "read", "articles" => "write" }, Hstore.column_defaults["permissions"])
- assert_equal({ "users" => "read", "articles" => "write" }, Hstore.new.permissions)
- ensure
- Hstore.reset_column_information
- end
+ def test_default
+ @connection.add_column "hstores", "permissions", :hstore, default: '"users"=>"read", "articles"=>"write"'
+ Hstore.reset_column_information
- def test_change_table_supports_hstore
- @connection.transaction do
- @connection.change_table("hstores") do |t|
- t.hstore "users", default: ""
- end
- Hstore.reset_column_information
- column = Hstore.columns_hash["users"]
- assert_equal :hstore, column.type
+ assert_equal({ "users" => "read", "articles" => "write" }, Hstore.column_defaults["permissions"])
+ assert_equal({ "users" => "read", "articles" => "write" }, Hstore.new.permissions)
+ ensure
+ Hstore.reset_column_information
+ end
- raise ActiveRecord::Rollback # reset the schema change
+ def test_change_table_supports_hstore
+ @connection.transaction do
+ @connection.change_table("hstores") do |t|
+ t.hstore "users", default: ""
end
- ensure
Hstore.reset_column_information
+ column = Hstore.columns_hash["users"]
+ assert_equal :hstore, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
end
+ ensure
+ Hstore.reset_column_information
+ end
- def test_hstore_migration
- hstore_migration = Class.new(ActiveRecord::Migration::Current) do
- def change
- change_table("hstores") do |t|
- t.hstore :keys
- end
+ def test_hstore_migration
+ hstore_migration = Class.new(ActiveRecord::Migration::Current) do
+ def change
+ change_table("hstores") do |t|
+ t.hstore :keys
end
end
-
- hstore_migration.new.suppress_messages do
- hstore_migration.migrate(:up)
- assert_includes @connection.columns(:hstores).map(&:name), "keys"
- hstore_migration.migrate(:down)
- assert_not_includes @connection.columns(:hstores).map(&:name), "keys"
- end
end
- def test_cast_value_on_write
- x = Hstore.new tags: { "bool" => true, "number" => 5 }
- assert_equal({ "bool" => true, "number" => 5 }, x.tags_before_type_cast)
- assert_equal({ "bool" => "true", "number" => "5" }, x.tags)
- x.save
- assert_equal({ "bool" => "true", "number" => "5" }, x.reload.tags)
+ hstore_migration.new.suppress_messages do
+ hstore_migration.migrate(:up)
+ assert_includes @connection.columns(:hstores).map(&:name), "keys"
+ hstore_migration.migrate(:down)
+ assert_not_includes @connection.columns(:hstores).map(&:name), "keys"
end
+ end
- def test_type_cast_hstore
- assert_equal({ "1" => "2" }, @type.deserialize("\"1\"=>\"2\""))
- assert_equal({}, @type.deserialize(""))
- assert_equal({ "key" => nil }, @type.deserialize("key => NULL"))
- assert_equal({ "c" => "}", '"a"' => 'b "a b' }, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b")))
- end
+ def test_cast_value_on_write
+ x = Hstore.new tags: { "bool" => true, "number" => 5 }
+ assert_equal({ "bool" => true, "number" => 5 }, x.tags_before_type_cast)
+ assert_equal({ "bool" => "true", "number" => "5" }, x.tags)
+ x.save
+ assert_equal({ "bool" => "true", "number" => "5" }, x.reload.tags)
+ end
- def test_with_store_accessors
- x = Hstore.new(language: "fr", timezone: "GMT")
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ def test_type_cast_hstore
+ assert_equal({ "1" => "2" }, @type.deserialize("\"1\"=>\"2\""))
+ assert_equal({}, @type.deserialize(""))
+ assert_equal({ "key" => nil }, @type.deserialize("key => NULL"))
+ assert_equal({ "c" => "}", '"a"' => 'b "a b' }, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b")))
+ end
- x.save!
- x = Hstore.first
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ def test_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- x.language = "de"
- x.save!
+ x.save!
+ x = Hstore.first
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- x = Hstore.first
- assert_equal "de", x.language
- assert_equal "GMT", x.timezone
- end
+ x.language = "de"
+ x.save!
- def test_duplication_with_store_accessors
- x = Hstore.new(language: "fr", timezone: "GMT")
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ x = Hstore.first
+ assert_equal "de", x.language
+ assert_equal "GMT", x.timezone
+ end
- y = x.dup
- assert_equal "fr", y.language
- assert_equal "GMT", y.timezone
- end
+ def test_duplication_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_yaml_round_trip_with_store_accessors
- x = Hstore.new(language: "fr", timezone: "GMT")
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ y = x.dup
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
- y = YAML.load(YAML.dump(x))
- assert_equal "fr", y.language
- assert_equal "GMT", y.timezone
- end
+ def test_yaml_round_trip_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_changes_in_place
- hstore = Hstore.create!(settings: { "one" => "two" })
- hstore.settings["three"] = "four"
- hstore.save!
- hstore.reload
+ y = YAML.load(YAML.dump(x))
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
- assert_equal "four", hstore.settings["three"]
- assert_not hstore.changed?
- end
+ def test_changes_in_place
+ hstore = Hstore.create!(settings: { "one" => "two" })
+ hstore.settings["three"] = "four"
+ hstore.save!
+ hstore.reload
- def test_dirty_from_user_equal
- settings = { "alongkey" => "anything", "key" => "value" }
- hstore = Hstore.create!(settings: settings)
+ assert_equal "four", hstore.settings["three"]
+ assert_not_predicate hstore, :changed?
+ end
- hstore.settings = { "key" => "value", "alongkey" => "anything" }
- assert_equal settings, hstore.settings
- refute hstore.changed?
- end
+ def test_dirty_from_user_equal
+ settings = { "alongkey" => "anything", "key" => "value" }
+ hstore = Hstore.create!(settings: settings)
- def test_hstore_dirty_from_database_equal
- settings = { "alongkey" => "anything", "key" => "value" }
- hstore = Hstore.create!(settings: settings)
- hstore.reload
+ hstore.settings = { "key" => "value", "alongkey" => "anything" }
+ assert_equal settings, hstore.settings
+ assert_not_predicate hstore, :changed?
+ end
- assert_equal settings, hstore.settings
- hstore.settings = settings
- refute hstore.changed?
- end
+ def test_hstore_dirty_from_database_equal
+ settings = { "alongkey" => "anything", "key" => "value" }
+ hstore = Hstore.create!(settings: settings)
+ hstore.reload
- def test_gen1
- assert_equal('" "=>""', @type.serialize(" " => ""))
- end
+ assert_equal settings, hstore.settings
+ hstore.settings = settings
+ assert_not_predicate hstore, :changed?
+ end
- def test_gen2
- assert_equal('","=>""', @type.serialize("," => ""))
- end
+ def test_gen1
+ assert_equal('" "=>""', @type.serialize(" " => ""))
+ end
- def test_gen3
- assert_equal('"="=>""', @type.serialize("=" => ""))
- end
+ def test_gen2
+ assert_equal('","=>""', @type.serialize("," => ""))
+ end
- def test_gen4
- assert_equal('">"=>""', @type.serialize(">" => ""))
- end
+ def test_gen3
+ assert_equal('"="=>""', @type.serialize("=" => ""))
+ end
- def test_parse1
- assert_equal({ "a" => nil, "b" => nil, "c" => "NuLl", "null" => "c" }, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
- end
+ def test_gen4
+ assert_equal('">"=>""', @type.serialize(">" => ""))
+ end
- def test_parse2
- assert_equal({ " " => " " }, @type.deserialize("\\ =>\\ "))
- end
+ def test_parse1
+ assert_equal({ "a" => nil, "b" => nil, "c" => "NuLl", "null" => "c" }, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
+ end
- def test_parse3
- assert_equal({ "=" => ">" }, @type.deserialize("==>>"))
- end
+ def test_parse2
+ assert_equal({ " " => " " }, @type.deserialize("\\ =>\\ "))
+ end
- def test_parse4
- assert_equal({ "=a" => "q=w" }, @type.deserialize('\=a=>q=w'))
- end
+ def test_parse3
+ assert_equal({ "=" => ">" }, @type.deserialize("==>>"))
+ end
- def test_parse5
- assert_equal({ "=a" => "q=w" }, @type.deserialize('"=a"=>q\=w'))
- end
+ def test_parse4
+ assert_equal({ "=a" => "q=w" }, @type.deserialize('\=a=>q=w'))
+ end
- def test_parse6
- assert_equal({ "\"a" => "q>w" }, @type.deserialize('"\"a"=>q>w'))
- end
+ def test_parse5
+ assert_equal({ "=a" => "q=w" }, @type.deserialize('"=a"=>q\=w'))
+ end
- def test_parse7
- assert_equal({ "\"a" => "q\"w" }, @type.deserialize('\"a=>q"w'))
- end
+ def test_parse6
+ assert_equal({ "\"a" => "q>w" }, @type.deserialize('"\"a"=>q>w'))
+ end
- def test_rewrite
- @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
- x = Hstore.first
- x.tags = { '"a\'' => "b" }
- assert x.save!
- end
+ def test_parse7
+ assert_equal({ "\"a" => "q\"w" }, @type.deserialize('\"a=>q"w'))
+ end
- def test_select
- @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
- x = Hstore.first
- assert_equal({ "1" => "2" }, x.tags)
- end
+ def test_rewrite
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.first
+ x.tags = { '"a\'' => "b" }
+ assert x.save!
+ end
- def test_array_cycle
- assert_array_cycle([{ "AA" => "BB", "CC" => "DD" }, { "AA" => nil }])
- end
+ def test_select
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.first
+ assert_equal({ "1" => "2" }, x.tags)
+ end
- def test_array_strings_with_quotes
- assert_array_cycle([{ "this has" => 'some "s that need to be escaped"' }])
- end
+ def test_array_cycle
+ assert_array_cycle([{ "AA" => "BB", "CC" => "DD" }, { "AA" => nil }])
+ end
- def test_array_strings_with_commas
- assert_array_cycle([{ "this,has" => "many,values" }])
- end
+ def test_array_strings_with_quotes
+ assert_array_cycle([{ "this has" => 'some "s that need to be escaped"' }])
+ end
- def test_array_strings_with_array_delimiters
- assert_array_cycle(["{" => "}"])
- end
+ def test_array_strings_with_commas
+ assert_array_cycle([{ "this,has" => "many,values" }])
+ end
- def test_array_strings_with_null_strings
- assert_array_cycle([{ "NULL" => "NULL" }])
- end
+ def test_array_strings_with_array_delimiters
+ assert_array_cycle(["{" => "}"])
+ end
- def test_contains_nils
- assert_array_cycle([{ "NULL" => nil }])
- end
+ def test_array_strings_with_null_strings
+ assert_array_cycle([{ "NULL" => "NULL" }])
+ end
- def test_select_multikey
- @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
- x = Hstore.first
- assert_equal({ "1" => "2", "2" => "3" }, x.tags)
- end
+ def test_contains_nils
+ assert_array_cycle([{ "NULL" => nil }])
+ end
- def test_create
- assert_cycle("a" => "b", "1" => "2")
- end
+ def test_select_multikey
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
+ x = Hstore.first
+ assert_equal({ "1" => "2", "2" => "3" }, x.tags)
+ end
- def test_nil
- assert_cycle("a" => nil)
- end
+ def test_create
+ assert_cycle("a" => "b", "1" => "2")
+ end
- def test_quotes
- assert_cycle("a" => 'b"ar', '1"foo' => "2")
- end
+ def test_nil
+ assert_cycle("a" => nil)
+ end
- def test_whitespace
- assert_cycle("a b" => "b ar", '1"foo' => "2")
- end
+ def test_quotes
+ assert_cycle("a" => 'b"ar', '1"foo' => "2")
+ end
- def test_backslash
- assert_cycle('a\\b' => 'b\\ar', '1"foo' => "2")
- end
+ def test_whitespace
+ assert_cycle("a b" => "b ar", '1"foo' => "2")
+ end
- def test_comma
- assert_cycle("a, b" => "bar", '1"foo' => "2")
- end
+ def test_backslash
+ assert_cycle('a\\b' => 'b\\ar', '1"foo' => "2")
+ end
- def test_arrow
- assert_cycle("a=>b" => "bar", '1"foo' => "2")
- end
+ def test_comma
+ assert_cycle("a, b" => "bar", '1"foo' => "2")
+ end
- def test_quoting_special_characters
- assert_cycle("ca" => "cà", "ac" => "àc")
- end
+ def test_arrow
+ assert_cycle("a=>b" => "bar", '1"foo' => "2")
+ end
- def test_multiline
- assert_cycle("a\nb" => "c\nd")
- end
+ def test_quoting_special_characters
+ assert_cycle("ca" => "cà", "ac" => "àc")
+ end
- class TagCollection
- def initialize(hash); @hash = hash end
- def to_hash; @hash end
- def self.load(hash); new(hash) end
- def self.dump(object); object.to_hash end
- end
+ def test_multiline
+ assert_cycle("a\nb" => "c\nd")
+ end
- class HstoreWithSerialize < Hstore
- serialize :tags, TagCollection
- end
+ class TagCollection
+ def initialize(hash); @hash = hash end
+ def to_hash; @hash end
+ def self.load(hash); new(hash) end
+ def self.dump(object); object.to_hash end
+ end
- def test_hstore_with_serialized_attributes
- HstoreWithSerialize.create! tags: TagCollection.new("one" => "two")
- record = HstoreWithSerialize.first
- assert_instance_of TagCollection, record.tags
- assert_equal({ "one" => "two" }, record.tags.to_hash)
- record.tags = TagCollection.new("three" => "four")
- record.save!
- assert_equal({ "three" => "four" }, HstoreWithSerialize.first.tags.to_hash)
- end
+ class HstoreWithSerialize < Hstore
+ serialize :tags, TagCollection
+ end
- def test_clone_hstore_with_serialized_attributes
- HstoreWithSerialize.create! tags: TagCollection.new("one" => "two")
- record = HstoreWithSerialize.first
- dupe = record.dup
- assert_equal({ "one" => "two" }, dupe.tags.to_hash)
- end
+ def test_hstore_with_serialized_attributes
+ HstoreWithSerialize.create! tags: TagCollection.new("one" => "two")
+ record = HstoreWithSerialize.first
+ assert_instance_of TagCollection, record.tags
+ assert_equal({ "one" => "two" }, record.tags.to_hash)
+ record.tags = TagCollection.new("three" => "four")
+ record.save!
+ assert_equal({ "three" => "four" }, HstoreWithSerialize.first.tags.to_hash)
+ end
- def test_schema_dump_with_shorthand
- output = dump_table_schema("hstores")
- assert_match %r[t\.hstore "tags",\s+default: {}], output
- end
+ def test_clone_hstore_with_serialized_attributes
+ HstoreWithSerialize.create! tags: TagCollection.new("one" => "two")
+ record = HstoreWithSerialize.first
+ dupe = record.dup
+ assert_equal({ "one" => "two" }, dupe.tags.to_hash)
+ end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("hstores")
+ assert_match %r[t\.hstore "tags",\s+default: {}], output
+ end
+
+ def test_supports_to_unsafe_h_values
+ assert_equal("\"hi\"=>\"hi\"", @type.serialize(FakeParameters.new))
+ end
- def test_supports_to_unsafe_h_values
- assert_equal("\"hi\"=>\"hi\"", @type.serialize(FakeParameters.new))
+ private
+ def assert_array_cycle(array)
+ # test creation
+ x = Hstore.create!(payload: array)
+ x.reload
+ assert_equal(array, x.payload)
+
+ # test updating
+ x = Hstore.create!(payload: [])
+ x.payload = array
+ x.save!
+ x.reload
+ assert_equal(array, x.payload)
end
- private
- def assert_array_cycle(array)
- # test creation
- x = Hstore.create!(payload: array)
- x.reload
- assert_equal(array, x.payload)
-
- # test updating
- x = Hstore.create!(payload: [])
- x.payload = array
- x.save!
- x.reload
- assert_equal(array, x.payload)
- end
+ def assert_cycle(hash)
+ # test creation
+ x = Hstore.create!(tags: hash)
+ x.reload
+ assert_equal(hash, x.tags)
- def assert_cycle(hash)
- # test creation
- x = Hstore.create!(tags: hash)
- x.reload
- assert_equal(hash, x.tags)
-
- # test updating
- x = Hstore.create!(tags: {})
- x.tags = hash
- x.save!
- x.reload
- assert_equal(hash, x.tags)
- end
- end
+ # test updating
+ x = Hstore.create!(tags: {})
+ x.tags = hash
+ x.save!
+ x.reload
+ assert_equal(hash, x.tags)
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
index 0b18c0c9d7..5e56ce8427 100644
--- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
@@ -13,6 +13,7 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
@connection.create_table(:postgresql_infinities) do |t|
t.float :float
t.datetime :datetime
+ t.date :date
end
end
@@ -43,11 +44,25 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
end
test "type casting infinity on a datetime column" do
+ record = PostgresqlInfinity.create!(datetime: "infinity")
+ record.reload
+ assert_equal Float::INFINITY, record.datetime
+
record = PostgresqlInfinity.create!(datetime: Float::INFINITY)
record.reload
assert_equal Float::INFINITY, record.datetime
end
+ test "type casting infinity on a date column" do
+ record = PostgresqlInfinity.create!(date: "infinity")
+ record.reload
+ assert_equal Float::INFINITY, record.date
+
+ record = PostgresqlInfinity.create!(date: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.date
+ end
+
test "update_all with infinity on a datetime column" do
record = PostgresqlInfinity.create!
PostgresqlInfinity.update_all(datetime: Float::INFINITY)
@@ -68,4 +83,28 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
PostgresqlInfinity.reset_column_information
end
end
+
+ test "where clause with infinite range on a datetime column" do
+ record = PostgresqlInfinity.create!(datetime: Time.current)
+
+ string = PostgresqlInfinity.where(datetime: "-infinity".."infinity")
+ assert_equal record, string.take
+
+ infinity = PostgresqlInfinity.where(datetime: -::Float::INFINITY..::Float::INFINITY)
+ assert_equal record, infinity.take
+
+ assert_equal infinity.to_sql, string.to_sql
+ end
+
+ test "where clause with infinite range on a date column" do
+ record = PostgresqlInfinity.create!(date: Date.current)
+
+ string = PostgresqlInfinity.where(date: "-infinity".."infinity")
+ assert_equal record, string.take
+
+ infinity = PostgresqlInfinity.where(date: -::Float::INFINITY..::Float::INFINITY)
+ assert_equal record, infinity.take
+
+ assert_equal infinity.to_sql, string.to_sql
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
index eca29f2892..8349ee6ee2 100644
--- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
@@ -31,10 +31,10 @@ class PostgresqlLtreeTest < ActiveRecord::PostgreSQLTestCase
column = Ltree.columns_hash["path"]
assert_equal :ltree, column.type
assert_equal "ltree", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = Ltree.type_for_attribute("path")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_write
diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb
index 563f0bbfae..be3590e8dd 100644
--- a/activerecord/test/cases/adapters/postgresql/money_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/money_test.rb
@@ -26,15 +26,15 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
assert_equal :money, column.type
assert_equal "money", column.sql_type
assert_equal 2, column.scale
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlMoney.type_for_attribute("wealth")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_default
- assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults["depth"]
- assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth
+ assert_equal BigDecimal("150.55"), PostgresqlMoney.column_defaults["depth"]
+ assert_equal BigDecimal("150.55"), PostgresqlMoney.new.depth
end
def test_money_values
@@ -65,7 +65,7 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
money = PostgresqlMoney.create(wealth: "987.65".dup)
assert_equal 987.65, money.wealth
- new_value = BigDecimal.new("123.45")
+ new_value = BigDecimal("123.45")
money.wealth = new_value
money.save!
money.reload
diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb
index f461544a85..736570451b 100644
--- a/activerecord/test/cases/adapters/postgresql/network_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/network_test.rb
@@ -24,30 +24,30 @@ class PostgresqlNetworkTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlNetworkAddress.columns_hash["cidr_address"]
assert_equal :cidr, column.type
assert_equal "cidr", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlNetworkAddress.type_for_attribute("cidr_address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_inet_column
column = PostgresqlNetworkAddress.columns_hash["inet_address"]
assert_equal :inet, column.type
assert_equal "inet", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlNetworkAddress.type_for_attribute("inet_address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_macaddr_column
column = PostgresqlNetworkAddress.columns_hash["mac_address"]
assert_equal :macaddr, column.type
assert_equal "macaddr", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlNetworkAddress.type_for_attribute("mac_address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_network_types
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index f199519d86..cbb6cd42b5 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -248,12 +248,12 @@ module ActiveRecord
def test_index_with_opclass
with_example_table do
- @connection.add_index "ex", "data varchar_pattern_ops"
- index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" }
- assert_equal "data varchar_pattern_ops", index.columns
+ @connection.add_index "ex", "data", opclass: "varchar_pattern_ops"
+ index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" }
+ assert_equal ["data"], index.columns
- @connection.remove_index "ex", "data varchar_pattern_ops"
- assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" }
+ @connection.remove_index "ex", "data"
+ assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" }
end
end
@@ -263,25 +263,25 @@ module ActiveRecord
end
def test_columns_for_distinct_one_order
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc"])
end
def test_columns_for_distinct_few_orders
- assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
+ assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
end
def test_columns_for_distinct_with_case
assert_equal(
- "posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0",
+ "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id",
["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"])
)
end
def test_columns_for_distinct_blank_not_nil_orders
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "])
end
@@ -290,23 +290,23 @@ module ActiveRecord
def order.to_sql
"posts.created_at desc"
end
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id", [order])
end
def test_columns_for_distinct_with_nulls
- assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"])
- assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"])
+ assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"])
+ assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"])
end
def test_columns_for_distinct_without_order_specifiers
- assert_equal "posts.title, posts.updater_id AS alias_0",
+ assert_equal "posts.updater_id AS alias_0, posts.title",
@connection.columns_for_distinct("posts.title", ["posts.updater_id"])
- assert_equal "posts.title, posts.updater_id AS alias_0",
+ assert_equal "posts.updater_id AS alias_0, posts.title",
@connection.columns_for_distinct("posts.title", ["posts.updater_id nulls last"])
- assert_equal "posts.title, posts.updater_id AS alias_0",
+ assert_equal "posts.updater_id AS alias_0, posts.title",
@connection.columns_for_distinct("posts.title", ["posts.updater_id nulls first"])
end
diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb
index b4a776d04d..261c24634e 100644
--- a/activerecord/test/cases/adapters/postgresql/range_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/range_test.rb
@@ -134,10 +134,10 @@ _SQL
end
def test_numrange_values
- assert_equal BigDecimal.new("0.1")..BigDecimal.new("0.2"), @first_range.num_range
- assert_equal BigDecimal.new("0.1")...BigDecimal.new("0.2"), @second_range.num_range
- assert_equal BigDecimal.new("0.1")...BigDecimal.new("Infinity"), @third_range.num_range
- assert_equal BigDecimal.new("-Infinity")...BigDecimal.new("Infinity"), @fourth_range.num_range
+ assert_equal BigDecimal("0.1")..BigDecimal("0.2"), @first_range.num_range
+ assert_equal BigDecimal("0.1")...BigDecimal("0.2"), @second_range.num_range
+ assert_equal BigDecimal("0.1")...BigDecimal("Infinity"), @third_range.num_range
+ assert_equal BigDecimal("-Infinity")...BigDecimal("Infinity"), @fourth_range.num_range
assert_nil @empty_range.num_range
end
@@ -232,16 +232,67 @@ _SQL
end
end
+ def test_create_tstzrange_preserve_usec
+ tstzrange = Time.parse("2010-01-01 14:30:00.670277 +0100")...Time.parse("2011-02-02 14:30:00.745125 CDT")
+ round_trip(@new_range, :tstz_range, tstzrange)
+ assert_equal @new_range.tstz_range, tstzrange
+ assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00.670277 UTC")...Time.parse("2011-02-02 19:30:00.745125 UTC")
+ end
+
+ def test_update_tstzrange_preserve_usec
+ assert_equal_round_trip(@first_range, :tstz_range,
+ Time.parse("2010-01-01 14:30:00.245124 CDT")...Time.parse("2011-02-02 14:30:00.451274 CET"))
+ assert_nil_round_trip(@first_range, :tstz_range,
+ Time.parse("2010-01-01 14:30:00.245124 +0100")...Time.parse("2010-01-01 13:30:00.245124 +0000"))
+ end
+
+ def test_create_tsrange_preseve_usec
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal_round_trip(@new_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0, 125435)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 225435))
+ end
+
+ def test_update_tsrange_preserve_usec
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal_round_trip(@first_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 224242))
+ assert_nil_round_trip(@first_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432))
+ end
+
+ def test_timezone_awareness_tsrange_preserve_usec
+ tz = "Pacific Time (US & Canada)"
+
+ in_time_zone tz do
+ PostgresqlRange.reset_column_information
+ time_string = "2017-09-26 07:30:59.132451 -0700"
+ time = Time.zone.parse(time_string)
+ assert time.usec > 0
+
+ record = PostgresqlRange.new(ts_range: time_string..time_string)
+ assert_equal time..time, record.ts_range
+ assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone
+ assert_equal time.usec, record.ts_range.begin.usec
+
+ record.save!
+ record.reload
+
+ assert_equal time..time, record.ts_range
+ assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone
+ assert_equal time.usec, record.ts_range.begin.usec
+ end
+ end
+
def test_create_numrange
assert_equal_round_trip(@new_range, :num_range,
- BigDecimal.new("0.5")...BigDecimal.new("1"))
+ BigDecimal("0.5")...BigDecimal("1"))
end
def test_update_numrange
assert_equal_round_trip(@first_range, :num_range,
- BigDecimal.new("0.5")...BigDecimal.new("1"))
+ BigDecimal("0.5")...BigDecimal("1"))
assert_nil_round_trip(@first_range, :num_range,
- BigDecimal.new("0.5")...BigDecimal.new("0.5"))
+ BigDecimal("0.5")...BigDecimal("0.5"))
end
def test_create_daterange
@@ -307,6 +358,18 @@ _SQL
end
end
+ def test_infinity_values
+ PostgresqlRange.create!(int4_range: 1..Float::INFINITY,
+ int8_range: -Float::INFINITY..0,
+ float_range: -Float::INFINITY..Float::INFINITY)
+
+ record = PostgresqlRange.first
+
+ assert_equal(1...Float::INFINITY, record.int4_range)
+ assert_equal(-Float::INFINITY...1, record.int8_range)
+ assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range)
+ end
+
private
def assert_equal_round_trip(range, attribute, value)
round_trip(range, attribute, value)
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index 5a64da028b..7ad03c194f 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -459,7 +459,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
assert_equal :btree, index_d.using
assert_equal :gin, index_e.using
- assert_equal :desc, index_d.orders[INDEX_D_COLUMN]
+ assert_equal :desc, index_d.orders
end
end
@@ -500,6 +500,66 @@ class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase
end
end
+class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "trains" do |t|
+ t.string :name
+ t.text :description
+ end
+ end
+
+ teardown do
+ @connection.drop_table "trains", if_exists: true
+ end
+
+ def test_string_opclass_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name text_pattern_ops, description text_pattern_ops)"
+
+ output = dump_table_schema "trains"
+
+ assert_match(/opclass: :text_pattern_ops/, output)
+ end
+
+ def test_non_default_opclass_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name, description text_pattern_ops)"
+
+ output = dump_table_schema "trains"
+
+ assert_match(/opclass: \{ description: :text_pattern_ops \}/, output)
+ end
+end
+
+class SchemaIndexNullsOrderTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "trains" do |t|
+ t.string :name
+ t.text :description
+ end
+ end
+
+ teardown do
+ @connection.drop_table "trains", if_exists: true
+ end
+
+ def test_nulls_order_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name NULLS FIRST, description)"
+ output = dump_table_schema "trains"
+ assert_match(/order: \{ name: "NULLS FIRST" \}/, output)
+ end
+
+ def test_non_default_order_with_nulls_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_desc ON trains USING btree(name DESC NULLS LAST, description)"
+ output = dump_table_schema "trains"
+ assert_match(/order: \{ name: "DESC NULLS LAST" \}/, output)
+ end
+end
+
class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase
setup do
@connection = ActiveRecord::Base.connection
@@ -534,7 +594,7 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCa
end
def test_decimal_defaults_in_new_schema_when_overriding_domain
- assert_equal BigDecimal.new("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed"
+ assert_equal BigDecimal("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed"
end
def test_bpchar_defaults_in_new_schema_when_overriding_domain
diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb
index df7875dbf2..83ea86be6d 100644
--- a/activerecord/test/cases/adapters/postgresql/serial_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb
@@ -24,14 +24,14 @@ class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlSerial.columns_hash["seq"]
assert_equal :integer, column.type
assert_equal "integer", column.sql_type
- assert column.serial?
+ assert_predicate 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?
+ assert_not_predicate column, :serial?
end
def test_schema_dump_with_shorthand
@@ -66,14 +66,14 @@ class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlBigSerial.columns_hash["seq"]
assert_equal :integer, column.type
assert_equal "bigint", column.sql_type
- assert column.serial?
+ assert_predicate 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?
+ assert_not_predicate column, :serial?
end
def test_schema_dump_with_shorthand
@@ -111,7 +111,7 @@ module SequenceNameDetectionTestCases
columns = @connection.columns(:foo)
columns.each do |column|
assert_equal :integer, column.type
- assert column.serial?
+ assert_predicate column, :serial?
end
end
@@ -121,4 +121,36 @@ module SequenceNameDetectionTestCases
assert_match %r{t\.bigserial\s+"bar_baz_id",\s+null: false$}, output
end
end
+
+ class LongerSequenceNameDetectionTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ def setup
+ @table_name = "long_table_name_to_test_sequence_name_detection_for_serial_cols"
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table @table_name, force: true do |t|
+ t.serial :seq
+ t.bigserial :bigseq
+ end
+ end
+
+ def teardown
+ @connection.drop_table @table_name, if_exists: true
+ end
+
+ def test_serial_columns
+ columns = @connection.columns(@table_name)
+ columns.each do |column|
+ assert_equal :integer, column.type
+ assert_predicate column, :serial?
+ end
+ end
+
+ def test_schema_dump_with_long_table_name
+ output = dump_table_schema @table_name
+ assert_match %r{create_table "#{@table_name}", force: :cascade}, output
+ assert_match %r{t\.serial\s+"seq",\s+null: false$}, output
+ assert_match %r{t\.bigserial\s+"bigseq",\s+null: false$}, output
+ end
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
index a3eb4f9e67..fef4b02b04 100644
--- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
@@ -23,7 +23,7 @@ module ActiveRecord
assert_equal "bar", cache["foo"]
pid = fork {
- lookup = cache["foo"];
+ lookup = cache["foo"]
exit!(!lookup)
}
diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
index f56adf4a5e..984b2f5ea4 100644
--- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
@@ -14,6 +14,7 @@ module ActiveRecord
setup do
@abort, Thread.abort_on_exception = Thread.abort_on_exception, false
+ Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception
@connection = ActiveRecord::Base.connection
@@ -31,6 +32,7 @@ module ActiveRecord
@connection.drop_table "samples", if_exists: true
Thread.abort_on_exception = @abort
+ Thread.report_on_exception = @original_report_on_exception
end
test "raises SerializationFailure when a serialization failure occurs" do
@@ -74,7 +76,7 @@ module ActiveRecord
Sample.transaction do
s1.lock!
barrier.wait
- s2.update_attributes value: 1
+ s2.update value: 1
end
end
@@ -82,7 +84,7 @@ module ActiveRecord
Sample.transaction do
s2.lock!
barrier.wait
- s1.update_attributes value: 2
+ s1.update value: 2
end
ensure
thread.join
@@ -91,6 +93,90 @@ module ActiveRecord
end
end
+ test "raises LockWaitTimeout when lock wait timeout exceeded" do
+ skip unless ActiveRecord::Base.connection.postgresql_version >= 90300
+ assert_raises(ActiveRecord::LockWaitTimeout) do
+ s = Sample.create!(value: 1)
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch1.count_down
+ latch2.wait
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch1.wait
+ Sample.connection.execute("SET lock_timeout = 1")
+ Sample.lock.find(s.id)
+ end
+ ensure
+ Sample.connection.execute("SET lock_timeout = DEFAULT")
+ latch2.count_down
+ thread.join
+ end
+ end
+ end
+
+ test "raises QueryCanceled when statement timeout exceeded" do
+ assert_raises(ActiveRecord::QueryCanceled) do
+ s = Sample.create!(value: 1)
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch1.count_down
+ latch2.wait
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch1.wait
+ Sample.connection.execute("SET statement_timeout = 1")
+ Sample.lock.find(s.id)
+ end
+ ensure
+ Sample.connection.execute("SET statement_timeout = DEFAULT")
+ latch2.count_down
+ thread.join
+ end
+ end
+ end
+
+ test "raises QueryCanceled when canceling statement due to user request" do
+ assert_raises(ActiveRecord::QueryCanceled) do
+ s = Sample.create!(value: 1)
+ latch = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch.count_down
+ sleep(0.5)
+ conn = Sample.connection
+ pid = conn.query_value("SELECT pid FROM pg_stat_activity WHERE query LIKE '% FOR UPDATE'")
+ conn.execute("SELECT pg_cancel_backend(#{pid})")
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch.wait
+ Sample.lock.find(s.id)
+ end
+ ensure
+ thread.join
+ end
+ end
+ end
+
private
def with_warning_suppression
diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
index 449023b6eb..8212ed4263 100644
--- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
@@ -30,6 +30,6 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase
big_range = 0..123456789123456789
assert_raises(ActiveModel::RangeError) { int_range.serialize(big_range) }
- assert_equal "[0,123456789123456789]", bigint_range.serialize(big_range)
+ assert_equal "[0,123456789123456789]", @connection.type_cast(bigint_range.serialize(big_range))
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
index 466d281e85..71d07e2f4c 100644
--- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -82,7 +82,7 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
UUIDType.reset_column_information
column = UUIDType.columns_hash["thingy"]
- assert column.array?
+ assert_predicate column, :array?
assert_equal "{}", column.default
schema = dump_table_schema "uuid_data_type"
@@ -93,10 +93,10 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
column = UUIDType.columns_hash["guid"]
assert_equal :uuid, column.type
assert_equal "uuid", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = UUIDType.type_for_attribute("guid")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_treat_blank_uuid_as_nil
@@ -178,7 +178,7 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
duplicate = klass.new(guid: record.guid)
assert record.guid.present? # Ensure we actually are testing a UUID
- assert_not duplicate.valid?
+ assert_not_predicate duplicate, :valid?
end
end
@@ -222,68 +222,66 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase
connection.execute "DROP FUNCTION IF EXISTS my_uuid_generator();"
end
- if ActiveRecord::Base.connection.supports_extensions?
- def test_id_is_uuid
- assert_equal :uuid, UUID.columns_hash["id"].type
- assert UUID.primary_key
- end
+ def test_id_is_uuid
+ assert_equal :uuid, UUID.columns_hash["id"].type
+ assert UUID.primary_key
+ end
- def test_id_has_a_default
- u = UUID.create
- assert_not_nil u.id
- end
+ def test_id_has_a_default
+ u = UUID.create
+ assert_not_nil u.id
+ end
- def test_auto_create_uuid
- u = UUID.create
- u.reload
- assert_not_nil u.other_uuid
- end
+ def test_auto_create_uuid
+ u = UUID.create
+ u.reload
+ assert_not_nil u.other_uuid
+ end
- def test_pk_and_sequence_for_uuid_primary_key
- pk, seq = connection.pk_and_sequence_for("pg_uuids")
- assert_equal "id", pk
- assert_nil seq
- end
+ def test_pk_and_sequence_for_uuid_primary_key
+ pk, seq = connection.pk_and_sequence_for("pg_uuids")
+ assert_equal "id", pk
+ assert_nil seq
+ end
- def test_schema_dumper_for_uuid_primary_key
- schema = dump_table_schema "pg_uuids"
- assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: -> { "uuid_generate_v1\(\)" }/, schema)
- assert_match(/t\.uuid "other_uuid", default: -> { "uuid_generate_v4\(\)" }/, schema)
- end
+ def test_schema_dumper_for_uuid_primary_key
+ schema = dump_table_schema "pg_uuids"
+ assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: -> { "uuid_generate_v1\(\)" }/, schema)
+ assert_match(/t\.uuid "other_uuid", default: -> { "uuid_generate_v4\(\)" }/, schema)
+ end
+
+ def test_schema_dumper_for_uuid_primary_key_with_custom_default
+ schema = dump_table_schema "pg_uuids_2"
+ assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema)
+ assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema)
+ end
- def test_schema_dumper_for_uuid_primary_key_with_custom_default
- schema = dump_table_schema "pg_uuids_2"
- assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema)
- assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema)
+ def test_schema_dumper_for_uuid_primary_key_default
+ schema = dump_table_schema "pg_uuids_3"
+ if connection.supports_pgcrypto_uuid?
+ assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "gen_random_uuid\(\)" }/, schema)
+ else
+ assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema)
end
+ end
+
+ def test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
- def test_schema_dumper_for_uuid_primary_key_default
- schema = dump_table_schema "pg_uuids_3"
- if connection.supports_pgcrypto_uuid?
- assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "gen_random_uuid\(\)" }/, schema)
- else
- assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema)
+ migration = Class.new(ActiveRecord::Migration[5.0]) do
+ def version; 101 end
+ def migrate(x)
+ create_table("pg_uuids_4", id: :uuid)
end
- end
+ end.new
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
- def test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration
- @verbose_was = ActiveRecord::Migration.verbose
- ActiveRecord::Migration.verbose = false
-
- migration = Class.new(ActiveRecord::Migration[5.0]) do
- def version; 101 end
- def migrate(x)
- create_table("pg_uuids_4", id: :uuid)
- end
- end.new
- ActiveRecord::Migrator.new(:up, [migration]).migrate
-
- schema = dump_table_schema "pg_uuids_4"
- assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema)
- ensure
- drop_table "pg_uuids_4"
- ActiveRecord::Migration.verbose = @verbose_was
- end
+ schema = dump_table_schema "pg_uuids_4"
+ assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema)
+ ensure
+ drop_table "pg_uuids_4"
+ ActiveRecord::Migration.verbose = @verbose_was
end
end
@@ -302,38 +300,36 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase
drop_table "pg_uuids"
end
- if ActiveRecord::Base.connection.supports_extensions?
- def test_id_allows_default_override_via_nil
- col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
- FROM pg_attribute a
- LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
- WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first
- assert_nil col_desc["default"]
- end
+ def test_id_allows_default_override_via_nil
+ col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
+ FROM pg_attribute a
+ LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
+ WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first
+ assert_nil col_desc["default"]
+ end
- def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil
- schema = dump_table_schema "pg_uuids"
- assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema)
- end
+ def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil
+ schema = dump_table_schema "pg_uuids"
+ assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema)
+ end
- def test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration
- @verbose_was = ActiveRecord::Migration.verbose
- ActiveRecord::Migration.verbose = false
-
- migration = Class.new(ActiveRecord::Migration[5.0]) do
- def version; 101 end
- def migrate(x)
- create_table("pg_uuids_4", id: :uuid, default: nil)
- end
- end.new
- ActiveRecord::Migrator.new(:up, [migration]).migrate
-
- schema = dump_table_schema "pg_uuids_4"
- assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema)
- ensure
- drop_table "pg_uuids_4"
- ActiveRecord::Migration.verbose = @verbose_was
- end
+ def test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+
+ migration = Class.new(ActiveRecord::Migration[5.0]) do
+ def version; 101 end
+ def migrate(x)
+ create_table("pg_uuids_4", id: :uuid, default: nil)
+ end
+ end.new
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ schema = dump_table_schema "pg_uuids_4"
+ assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema)
+ ensure
+ drop_table "pg_uuids_4"
+ ActiveRecord::Migration.verbose = @verbose_was
end
end
@@ -367,23 +363,21 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase
drop_table "pg_uuid_posts"
end
- if ActiveRecord::Base.connection.supports_extensions?
- def test_collection_association_with_uuid
- post = UuidPost.create!
- comment = post.uuid_comments.create!
- assert post.uuid_comments.find(comment.id)
- end
+ def test_collection_association_with_uuid
+ post = UuidPost.create!
+ comment = post.uuid_comments.create!
+ assert post.uuid_comments.find(comment.id)
+ end
- def test_find_with_uuid
- UuidPost.create!
- assert_raise ActiveRecord::RecordNotFound do
- UuidPost.find(123456)
- end
+ def test_find_with_uuid
+ UuidPost.create!
+ assert_raise ActiveRecord::RecordNotFound do
+ UuidPost.find(123456)
end
+ end
- def test_find_by_with_uuid
- UuidPost.create!
- assert_nil UuidPost.find_by(id: 789)
- end
+ def test_find_by_with_uuid
+ UuidPost.create!
+ assert_nil UuidPost.find_by(id: 789)
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
index 3b081d34e1..b6d2ccdb53 100644
--- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
@@ -1,23 +1,23 @@
# frozen_string_literal: true
require "cases/helper"
-require "models/developer"
-require "models/computer"
+require "models/author"
+require "models/post"
class SQLite3ExplainTest < ActiveRecord::SQLite3TestCase
- fixtures :developers
+ fixtures :authors
def test_explain_for_one_query
- explain = Developer.where(id: 1).explain
- assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\? \[\["id", 1\]\]|1)), explain
- assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
+ explain = Author.where(id: 1).explain
+ assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\? \[\["id", 1\]\]|1)), explain
+ assert_match(/(SEARCH )?TABLE authors USING (INTEGER )?PRIMARY KEY/, explain)
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" = (?:\? \[\["id", 1\]\]|1)), explain
- assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
- assert_match %r(EXPLAIN for: SELECT "audit_logs"\.\* FROM "audit_logs" WHERE "audit_logs"\."developer_id" = (?:\? \[\["developer_id", 1\]\]|1)), explain
- assert_match(/(SCAN )?TABLE audit_logs/, explain)
+ explain = Author.where(id: 1).includes(:posts).explain
+ assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\? \[\["id", 1\]\]|1)), explain
+ assert_match(/(SEARCH )?TABLE authors USING (INTEGER )?PRIMARY KEY/, explain)
+ assert_match %r(EXPLAIN for: SELECT "posts"\.\* FROM "posts" WHERE "posts"\."author_id" = (?:\? \[\["author_id", 1\]\]|1)), explain
+ assert_match(/(SCAN )?TABLE posts/, explain)
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/json_test.rb b/activerecord/test/cases/adapters/sqlite3/json_test.rb
new file mode 100644
index 0000000000..6f247fcd22
--- /dev/null
+++ b/activerecord/test/cases/adapters/sqlite3/json_test.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "cases/json_shared_test_cases"
+
+class SQLite3JSONTest < ActiveRecord::SQLite3TestCase
+ include JSONSharedTestCases
+
+ def setup
+ super
+ @connection.create_table("json_data_type") do |t|
+ t.json "payload", default: {}
+ t.json "settings"
+ end
+ end
+
+ def test_default
+ @connection.add_column "json_data_type", "permissions", column_type, default: { "users": "read", "posts": ["read", "write"] }
+ klass.reset_column_information
+
+ assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.column_defaults["permissions"])
+ assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.new.permissions)
+ end
+
+ private
+ def column_type
+ :json
+ end
+end
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index de422fad23..1c85ff5674 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -38,7 +38,7 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase
end
def test_type_cast_bigdecimal
- bd = BigDecimal.new "10.0"
+ bd = BigDecimal "10.0"
assert_equal bd.to_f, @conn.type_cast(bd)
end
@@ -55,4 +55,11 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase
assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value))
end
+
+ def test_quoted_time_normalizes_date_qualified_time
+ value = ::Time.utc(2018, 3, 11, 12, 30, 0, 999999)
+ type = ActiveRecord::Type::Time.new
+
+ assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value))
+ 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 2b51a32db6..7e0ce3a28e 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -269,14 +269,6 @@ module ActiveRecord
end
end
- def test_indexes_logs_name
- with_example_table do
- assert_logged [["PRAGMA index_list(\"ex\")", "SCHEMA", []]] do
- assert_deprecated { @conn.indexes("ex", "hello") }
- end
- end
- end
-
def test_table_exists_logs_name
with_example_table do
sql = <<-SQL
@@ -368,16 +360,126 @@ module ActiveRecord
end
end
+ class Barcode < ActiveRecord::Base
+ self.primary_key = "code"
+ end
+
+ def test_copy_table_with_existing_records_have_custom_primary_key
+ connection = Barcode.connection
+ connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true) do |t|
+ t.text :other_attr
+ end
+ code = "214fe0c2-dd47-46df-b53b-66090b3c1d40"
+ Barcode.create!(code: code, other_attr: "xxx")
+
+ connection.remove_column("barcodes", "other_attr")
+
+ assert_equal code, Barcode.first.id
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_copy_table_with_composite_primary_keys
+ connection = Barcode.connection
+ connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t|
+ t.string :region
+ t.string :code
+ t.text :other_attr
+ end
+ region = "US"
+ code = "214fe0c2-dd47-46df-b53b-66090b3c1d40"
+ Barcode.create!(region: region, code: code, other_attr: "xxx")
+
+ connection.remove_column("barcodes", "other_attr")
+
+ assert_equal ["region", "code"], connection.primary_keys("barcodes")
+
+ barcode = Barcode.first
+ assert_equal region, barcode.region
+ assert_equal code, barcode.code
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_custom_primary_key_in_create_table
+ connection = Barcode.connection
+ connection.create_table :barcodes, id: false, force: true do |t|
+ t.primary_key :id, :string
+ end
+
+ assert_equal "id", connection.primary_key("barcodes")
+
+ custom_pk = Barcode.columns_hash["id"]
+
+ assert_equal :string, custom_pk.type
+ assert_not custom_pk.null
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_custom_primary_key_in_change_table
+ connection = Barcode.connection
+ connection.create_table :barcodes, id: false, force: true do |t|
+ t.integer :dummy
+ end
+ connection.change_table :barcodes do |t|
+ t.primary_key :id, :string
+ end
+
+ assert_equal "id", connection.primary_key("barcodes")
+
+ custom_pk = Barcode.columns_hash["id"]
+
+ assert_equal :string, custom_pk.type
+ assert_not custom_pk.null
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_add_column_with_custom_primary_key
+ connection = Barcode.connection
+ connection.create_table :barcodes, id: false, force: true do |t|
+ t.integer :dummy
+ end
+ connection.add_column :barcodes, :id, :string, primary_key: true
+
+ assert_equal "id", connection.primary_key("barcodes")
+
+ custom_pk = Barcode.columns_hash["id"]
+
+ assert_equal :string, custom_pk.type
+ assert_not custom_pk.null
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_remove_column_preserves_partial_indexes
+ connection = Barcode.connection
+ connection.create_table :barcodes, force: true do |t|
+ t.string :code
+ t.string :region
+ t.boolean :bool_attr
+
+ t.index :code, unique: true, where: :bool_attr, name: "partial"
+ end
+ connection.remove_column :barcodes, :region
+
+ index = connection.indexes("barcodes").find { |idx| idx.name == "partial" }
+ assert_equal "bool_attr", index.where
+ ensure
+ Barcode.reset_column_information
+ end
+
def test_supports_extensions
assert_not @conn.supports_extensions?, "does not support extensions"
end
def test_respond_to_enable_extension
- assert @conn.respond_to?(:enable_extension)
+ assert_respond_to @conn, :enable_extension
end
def test_respond_to_disable_extension
- assert @conn.respond_to?(:disable_extension)
+ assert_respond_to @conn, :disable_extension
end
def test_statement_closed
@@ -398,6 +500,10 @@ module ActiveRecord
end
end
+ def test_deprecate_valid_alter_table_type
+ assert_deprecated { @conn.valid_alter_table_type?(:string) }
+ end
+
private
def assert_logged(logs)
diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
index 42b3841d41..61002435a4 100644
--- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
@@ -10,7 +10,7 @@ class SQLite3StatementPoolTest < ActiveRecord::SQLite3TestCase
assert_equal "bar", cache["foo"]
pid = fork {
- lookup = cache["foo"];
+ lookup = cache["foo"]
exit!(!lookup)
}
diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb
index 7f654ec6f6..fbdf2ada4b 100644
--- a/activerecord/test/cases/aggregations_test.rb
+++ b/activerecord/test/cases/aggregations_test.rb
@@ -27,7 +27,7 @@ class AggregationsTest < ActiveRecord::TestCase
def test_immutable_value_objects
customers(:david).balance = Money.new(100)
- assert_raise(RuntimeError) { customers(:david).balance.instance_eval { @amount = 20 } }
+ assert_raise(frozen_error_class) { customers(:david).balance.instance_eval { @amount = 20 } }
end
def test_inferred_mapping
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index 83974f327e..f05dcac7dd 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -48,7 +48,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" }
- assert_equal 7, ActiveRecord::Migrator::current_version
+ assert_equal 7, @connection.migration_context.current_version
end
def test_schema_define_w_table_name_prefix
@@ -64,7 +64,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
t.column :flavor, :string
end
end
- assert_equal 7, ActiveRecord::Migrator::current_version
+ assert_equal 7, @connection.migration_context.current_version
ensure
ActiveRecord::Base.table_name_prefix = old_table_name_prefix
ActiveRecord::SchemaMigration.table_name = table_name
@@ -116,8 +116,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
end
end
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
+ assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
end
def test_timestamps_without_null_set_null_to_false_on_change_table
@@ -129,8 +129,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
end
end
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
+ assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
end
def test_timestamps_without_null_set_null_to_false_on_add_timestamps
@@ -139,7 +139,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
add_timestamps :has_timestamps, default: Time.now
end
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
+ assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
end
end
diff --git a/activerecord/test/cases/arel/attributes/attribute_test.rb b/activerecord/test/cases/arel/attributes/attribute_test.rb
new file mode 100644
index 0000000000..52573021a5
--- /dev/null
+++ b/activerecord/test/cases/arel/attributes/attribute_test.rb
@@ -0,0 +1,1015 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "ostruct"
+
+module Arel
+ module Attributes
+ class AttributeTest < Arel::Spec
+ describe "#not_eq" do
+ it "should create a NotEqual node" do
+ relation = Table.new(:users)
+ relation[:id].not_eq(10).must_be_kind_of Nodes::NotEqual
+ end
+
+ it "should generate != in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" != 10
+ }
+ end
+
+ it "should handle nil" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq(nil)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" IS NOT NULL
+ }
+ end
+ end
+
+ describe "#not_eq_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].not_eq_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 OR "users"."id" != 2)
+ }
+ end
+ end
+
+ describe "#not_eq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].not_eq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 AND "users"."id" != 2)
+ }
+ end
+ end
+
+ describe "#gt" do
+ it "should create a GreaterThan node" do
+ relation = Table.new(:users)
+ relation[:id].gt(10).must_be_kind_of Nodes::GreaterThan
+ end
+
+ it "should generate > in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gt(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" > 10
+ }
+ end
+
+ it "should handle comparing with a subquery" do
+ users = Table.new(:users)
+
+ avg = users.project(users[:karma].average)
+ mgr = users.project(Arel.star).where(users[:karma].gt(avg))
+
+ mgr.to_sql.must_be_like %{
+ SELECT * FROM "users" WHERE "users"."karma" > (SELECT AVG("users"."karma") FROM "users")
+ }
+ end
+
+ it "should accept various data types." do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].gt("fake_name")
+ mgr.to_sql.must_match %{"users"."name" > 'fake_name'}
+
+ current_time = ::Time.now
+ mgr.where relation[:created_at].gt(current_time)
+ mgr.to_sql.must_match %{"users"."created_at" > '#{current_time}'}
+ end
+ end
+
+ describe "#gt_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].gt_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gt_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" > 1 OR "users"."id" > 2)
+ }
+ end
+ end
+
+ describe "#gt_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].gt_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gt_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" > 1 AND "users"."id" > 2)
+ }
+ end
+ end
+
+ describe "#gteq" do
+ it "should create a GreaterThanOrEqual node" do
+ relation = Table.new(:users)
+ relation[:id].gteq(10).must_be_kind_of Nodes::GreaterThanOrEqual
+ end
+
+ it "should generate >= in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gteq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" >= 10
+ }
+ end
+
+ it "should accept various data types." do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].gteq("fake_name")
+ mgr.to_sql.must_match %{"users"."name" >= 'fake_name'}
+
+ current_time = ::Time.now
+ mgr.where relation[:created_at].gteq(current_time)
+ mgr.to_sql.must_match %{"users"."created_at" >= '#{current_time}'}
+ end
+ end
+
+ describe "#gteq_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].gteq_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gteq_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 1 OR "users"."id" >= 2)
+ }
+ end
+ end
+
+ describe "#gteq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].gteq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gteq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 1 AND "users"."id" >= 2)
+ }
+ end
+ end
+
+ describe "#lt" do
+ it "should create a LessThan node" do
+ relation = Table.new(:users)
+ relation[:id].lt(10).must_be_kind_of Nodes::LessThan
+ end
+
+ it "should generate < in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lt(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" < 10
+ }
+ end
+
+ it "should accept various data types." do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].lt("fake_name")
+ mgr.to_sql.must_match %{"users"."name" < 'fake_name'}
+
+ current_time = ::Time.now
+ mgr.where relation[:created_at].lt(current_time)
+ mgr.to_sql.must_match %{"users"."created_at" < '#{current_time}'}
+ end
+ end
+
+ describe "#lt_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].lt_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lt_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" < 1 OR "users"."id" < 2)
+ }
+ end
+ end
+
+ describe "#lt_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].lt_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lt_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" < 1 AND "users"."id" < 2)
+ }
+ end
+ end
+
+ describe "#lteq" do
+ it "should create a LessThanOrEqual node" do
+ relation = Table.new(:users)
+ relation[:id].lteq(10).must_be_kind_of Nodes::LessThanOrEqual
+ end
+
+ it "should generate <= in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lteq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" <= 10
+ }
+ end
+
+ it "should accept various data types." do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].lteq("fake_name")
+ mgr.to_sql.must_match %{"users"."name" <= 'fake_name'}
+
+ current_time = ::Time.now
+ mgr.where relation[:created_at].lteq(current_time)
+ mgr.to_sql.must_match %{"users"."created_at" <= '#{current_time}'}
+ end
+ end
+
+ describe "#lteq_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].lteq_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lteq_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" <= 1 OR "users"."id" <= 2)
+ }
+ end
+ end
+
+ describe "#lteq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].lteq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lteq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" <= 1 AND "users"."id" <= 2)
+ }
+ end
+ end
+
+ describe "#average" do
+ it "should create a AVG node" do
+ relation = Table.new(:users)
+ relation[:id].average.must_be_kind_of Nodes::Avg
+ end
+
+ it "should generate the proper SQL" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id].average
+ mgr.to_sql.must_be_like %{
+ SELECT AVG("users"."id")
+ FROM "users"
+ }
+ end
+ end
+
+ describe "#maximum" do
+ it "should create a MAX node" do
+ relation = Table.new(:users)
+ relation[:id].maximum.must_be_kind_of Nodes::Max
+ end
+
+ it "should generate proper SQL" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id].maximum
+ mgr.to_sql.must_be_like %{
+ SELECT MAX("users"."id")
+ FROM "users"
+ }
+ end
+ end
+
+ describe "#minimum" do
+ it "should create a Min node" do
+ relation = Table.new(:users)
+ relation[:id].minimum.must_be_kind_of Nodes::Min
+ end
+
+ it "should generate proper SQL" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id].minimum
+ mgr.to_sql.must_be_like %{
+ SELECT MIN("users"."id")
+ FROM "users"
+ }
+ end
+ end
+
+ describe "#sum" do
+ it "should create a SUM node" do
+ relation = Table.new(:users)
+ relation[:id].sum.must_be_kind_of Nodes::Sum
+ end
+
+ it "should generate the proper SQL" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id].sum
+ mgr.to_sql.must_be_like %{
+ SELECT SUM("users"."id")
+ FROM "users"
+ }
+ end
+ end
+
+ describe "#count" do
+ it "should return a count node" do
+ relation = Table.new(:users)
+ relation[:id].count.must_be_kind_of Nodes::Count
+ end
+
+ it "should take a distinct param" do
+ relation = Table.new(:users)
+ count = relation[:id].count(nil)
+ count.must_be_kind_of Nodes::Count
+ count.distinct.must_be_nil
+ end
+ end
+
+ describe "#eq" do
+ it "should return an equality node" do
+ attribute = Attribute.new nil, nil
+ equality = attribute.eq 1
+ equality.left.must_equal attribute
+ equality.right.val.must_equal 1
+ equality.must_be_kind_of Nodes::Equality
+ end
+
+ it "should generate = in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" = 10
+ }
+ end
+
+ it "should handle nil" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq(nil)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" IS NULL
+ }
+ end
+ end
+
+ describe "#eq_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].eq_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 OR "users"."id" = 2)
+ }
+ end
+
+ it "should not eat input" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ values = [1, 2]
+ mgr.where relation[:id].eq_any(values)
+ values.must_equal [1, 2]
+ end
+ end
+
+ describe "#eq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].eq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 AND "users"."id" = 2)
+ }
+ end
+
+ it "should not eat input" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ values = [1, 2]
+ mgr.where relation[:id].eq_all(values)
+ values.must_equal [1, 2]
+ end
+ end
+
+ describe "#matches" do
+ it "should create a Matches node" do
+ relation = Table.new(:users)
+ relation[:name].matches("%bacon%").must_be_kind_of Nodes::Matches
+ end
+
+ it "should generate LIKE in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].matches("%bacon%")
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."name" LIKE '%bacon%'
+ }
+ end
+ end
+
+ describe "#matches_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:name].matches_any(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].matches_any(["%chunky%", "%bacon%"])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" LIKE '%chunky%' OR "users"."name" LIKE '%bacon%')
+ }
+ end
+ end
+
+ describe "#matches_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:name].matches_all(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].matches_all(["%chunky%", "%bacon%"])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" LIKE '%chunky%' AND "users"."name" LIKE '%bacon%')
+ }
+ end
+ end
+
+ describe "#does_not_match" do
+ it "should create a DoesNotMatch node" do
+ relation = Table.new(:users)
+ relation[:name].does_not_match("%bacon%").must_be_kind_of Nodes::DoesNotMatch
+ end
+
+ it "should generate NOT LIKE in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match("%bacon%")
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."name" NOT LIKE '%bacon%'
+ }
+ end
+ end
+
+ describe "#does_not_match_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:name].does_not_match_any(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match_any(["%chunky%", "%bacon%"])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" NOT LIKE '%chunky%' OR "users"."name" NOT LIKE '%bacon%')
+ }
+ end
+ end
+
+ describe "#does_not_match_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:name].does_not_match_all(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" NOT LIKE '%chunky%' AND "users"."name" NOT LIKE '%bacon%')
+ }
+ end
+ end
+
+ describe "with a range" do
+ it "can be constructed with a standard range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(1..3)
+
+ node.must_equal Nodes::Between.new(
+ attribute,
+ Nodes::And.new([
+ Nodes::Casted.new(1, attribute),
+ Nodes::Casted.new(3, attribute)
+ ])
+ )
+ end
+
+ it "can be constructed with a range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(-::Float::INFINITY..3)
+
+ node.must_equal Nodes::LessThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ end
+
+ it "can be constructed with a quoted range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(quoted_range(-::Float::INFINITY, 3, false))
+
+ node.must_equal Nodes::LessThanOrEqual.new(
+ attribute,
+ Nodes::Quoted.new(3)
+ )
+ end
+
+ it "can be constructed with an exclusive range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(-::Float::INFINITY...3)
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ end
+
+ it "can be constructed with a quoted exclusive range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(quoted_range(-::Float::INFINITY, 3, true))
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Quoted.new(3)
+ )
+ end
+
+ it "can be constructed with an infinite range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(-::Float::INFINITY..::Float::INFINITY)
+
+ node.must_equal Nodes::NotIn.new(attribute, [])
+ end
+
+ it "can be constructed with a quoted infinite range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(quoted_range(-::Float::INFINITY, ::Float::INFINITY, false))
+
+ node.must_equal Nodes::NotIn.new(attribute, [])
+ end
+
+
+ it "can be constructed with a range ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(0..::Float::INFINITY)
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+
+ it "can be constructed with a quoted range ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(quoted_range(0, ::Float::INFINITY, false))
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Quoted.new(0)
+ )
+ end
+
+ it "can be constructed with an exclusive range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(0...3)
+
+ node.must_equal Nodes::And.new([
+ Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ ),
+ Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ ])
+ end
+
+ def quoted_range(begin_val, end_val, exclude)
+ OpenStruct.new(
+ begin: Nodes::Quoted.new(begin_val),
+ end: Nodes::Quoted.new(end_val),
+ exclude_end?: exclude,
+ )
+ end
+ end
+
+ describe "#in" do
+ it "can be constructed with a subquery" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"])
+ attribute = Attribute.new nil, nil
+
+ node = attribute.in(mgr)
+
+ node.must_equal Nodes::In.new(attribute, mgr.ast)
+ end
+
+ it "can be constructed with a list" do
+ attribute = Attribute.new nil, nil
+ node = attribute.in([1, 2, 3])
+
+ node.must_equal Nodes::In.new(
+ attribute,
+ [
+ Nodes::Casted.new(1, attribute),
+ Nodes::Casted.new(2, attribute),
+ Nodes::Casted.new(3, attribute),
+ ]
+ )
+ end
+
+ it "can be constructed with a random object" do
+ attribute = Attribute.new nil, nil
+ random_object = Object.new
+ node = attribute.in(random_object)
+
+ node.must_equal Nodes::In.new(
+ attribute,
+ Nodes::Casted.new(random_object, attribute)
+ )
+ end
+
+ it "should generate IN in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].in([1, 2, 3])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" IN (1, 2, 3)
+ }
+ end
+ end
+
+ describe "#in_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].in_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].in_any([[1, 2], [3, 4]])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" IN (1, 2) OR "users"."id" IN (3, 4))
+ }
+ end
+ end
+
+ describe "#in_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].in_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].in_all([[1, 2], [3, 4]])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" IN (1, 2) AND "users"."id" IN (3, 4))
+ }
+ end
+ end
+
+ describe "with a range" do
+ it "can be constructed with a standard range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(1..3)
+
+ node.must_equal Nodes::Grouping.new(Nodes::Or.new(
+ Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(1, attribute)
+ ),
+ Nodes::GreaterThan.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ ))
+ end
+
+ it "can be constructed with a range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(-::Float::INFINITY..3)
+
+ node.must_equal Nodes::GreaterThan.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ end
+
+ it "can be constructed with an exclusive range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(-::Float::INFINITY...3)
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ end
+
+ it "can be constructed with an infinite range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(-::Float::INFINITY..::Float::INFINITY)
+
+ node.must_equal Nodes::In.new(attribute, [])
+ end
+
+ it "can be constructed with a range ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(0..::Float::INFINITY)
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+
+ it "can be constructed with an exclusive range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(0...3)
+
+ node.must_equal Nodes::Grouping.new(Nodes::Or.new(
+ Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ ),
+ Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ ))
+ end
+ end
+
+ describe "#not_in" do
+ it "can be constructed with a subquery" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"])
+ attribute = Attribute.new nil, nil
+
+ node = attribute.not_in(mgr)
+
+ node.must_equal Nodes::NotIn.new(attribute, mgr.ast)
+ end
+
+ it "can be constructed with a Union" do
+ relation = Table.new(:users)
+ mgr1 = relation.project(relation[:id])
+ mgr2 = relation.project(relation[:id])
+
+ union = mgr1.union(mgr2)
+ node = relation[:id].in(union)
+ node.to_sql.must_be_like %{
+ "users"."id" IN (( SELECT "users"."id" FROM "users" UNION SELECT "users"."id" FROM "users" ))
+ }
+ end
+
+ it "can be constructed with a list" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_in([1, 2, 3])
+
+ node.must_equal Nodes::NotIn.new(
+ attribute,
+ [
+ Nodes::Casted.new(1, attribute),
+ Nodes::Casted.new(2, attribute),
+ Nodes::Casted.new(3, attribute),
+ ]
+ )
+ end
+
+ it "can be constructed with a random object" do
+ attribute = Attribute.new nil, nil
+ random_object = Object.new
+ node = attribute.not_in(random_object)
+
+ node.must_equal Nodes::NotIn.new(
+ attribute,
+ Nodes::Casted.new(random_object, attribute)
+ )
+ end
+
+ it "should generate NOT IN in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_in([1, 2, 3])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" NOT IN (1, 2, 3)
+ }
+ end
+ end
+
+ describe "#not_in_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].not_in_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_in_any([[1, 2], [3, 4]])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" NOT IN (1, 2) OR "users"."id" NOT IN (3, 4))
+ }
+ end
+ end
+
+ describe "#not_in_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].not_in_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_in_all([[1, 2], [3, 4]])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" NOT IN (1, 2) AND "users"."id" NOT IN (3, 4))
+ }
+ end
+ end
+
+ describe "#eq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].eq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 AND "users"."id" = 2)
+ }
+ end
+ end
+
+ describe "#asc" do
+ it "should create an Ascending node" do
+ relation = Table.new(:users)
+ relation[:id].asc.must_be_kind_of Nodes::Ascending
+ end
+
+ it "should generate ASC in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.order relation[:id].asc
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" ORDER BY "users"."id" ASC
+ }
+ end
+ end
+
+ describe "#desc" do
+ it "should create a Descending node" do
+ relation = Table.new(:users)
+ relation[:id].desc.must_be_kind_of Nodes::Descending
+ end
+
+ it "should generate DESC in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.order relation[:id].desc
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" ORDER BY "users"."id" DESC
+ }
+ end
+ end
+
+ describe "equality" do
+ describe "#to_sql" do
+ it "should produce sql" do
+ table = Table.new :users
+ condition = table["id"].eq 1
+ condition.to_sql.must_equal '"users"."id" = 1'
+ end
+ end
+ end
+
+ describe "type casting" do
+ it "does not type cast by default" do
+ table = Table.new(:foo)
+ condition = table["id"].eq("1")
+
+ refute table.able_to_type_cast?
+ condition.to_sql.must_equal %("foo"."id" = '1')
+ end
+
+ it "type casts when given an explicit caster" do
+ fake_caster = Object.new
+ def fake_caster.type_cast_for_database(attr_name, value)
+ if attr_name == "id"
+ value.to_i
+ else
+ value
+ end
+ end
+ table = Table.new(:foo, type_caster: fake_caster)
+ condition = table["id"].eq("1").and(table["other_id"].eq("2"))
+
+ assert table.able_to_type_cast?
+ condition.to_sql.must_equal %("foo"."id" = 1 AND "foo"."other_id" = '2')
+ end
+
+ it "does not type cast SqlLiteral nodes" do
+ fake_caster = Object.new
+ def fake_caster.type_cast_for_database(attr_name, value)
+ value.to_i
+ end
+ table = Table.new(:foo, type_caster: fake_caster)
+ condition = table["id"].eq(Arel.sql("(select 1)"))
+
+ assert table.able_to_type_cast?
+ condition.to_sql.must_equal %("foo"."id" = (select 1))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/attributes_test.rb b/activerecord/test/cases/arel/attributes_test.rb
new file mode 100644
index 0000000000..b00af4bd29
--- /dev/null
+++ b/activerecord/test/cases/arel/attributes_test.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ describe "Attributes" do
+ it "responds to lower" do
+ relation = Table.new(:users)
+ attribute = relation[:foo]
+ node = attribute.lower
+ assert_equal "LOWER", node.name
+ assert_equal [attribute], node.expressions
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Attribute.new("foo", "bar"), Attribute.new("foo", "bar")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Attribute.new("foo", "bar"), Attribute.new("foo", "baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+
+ describe "for" do
+ it "deals with unknown column types" do
+ column = Struct.new(:type).new :crazy
+ Attributes.for(column).must_equal Attributes::Undefined
+ end
+
+ it "returns the correct constant for strings" do
+ [:string, :text, :binary].each do |type|
+ column = Struct.new(:type).new type
+ Attributes.for(column).must_equal Attributes::String
+ end
+ end
+
+ it "returns the correct constant for ints" do
+ column = Struct.new(:type).new :integer
+ Attributes.for(column).must_equal Attributes::Integer
+ end
+
+ it "returns the correct constant for floats" do
+ column = Struct.new(:type).new :float
+ Attributes.for(column).must_equal Attributes::Float
+ end
+
+ it "returns the correct constant for decimals" do
+ column = Struct.new(:type).new :decimal
+ Attributes.for(column).must_equal Attributes::Decimal
+ end
+
+ it "returns the correct constant for boolean" do
+ column = Struct.new(:type).new :boolean
+ Attributes.for(column).must_equal Attributes::Boolean
+ end
+
+ it "returns the correct constant for time" do
+ [:date, :datetime, :timestamp, :time].each do |type|
+ column = Struct.new(:type).new type
+ Attributes.for(column).must_equal Attributes::Time
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/collectors/bind_test.rb b/activerecord/test/cases/arel/collectors/bind_test.rb
new file mode 100644
index 0000000000..ffa9b15f66
--- /dev/null
+++ b/activerecord/test/cases/arel/collectors/bind_test.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "arel/collectors/bind"
+
+module Arel
+ module Collectors
+ class TestBind < Arel::Test
+ def setup
+ @conn = FakeRecord::Base.new
+ @visitor = Visitors::ToSql.new @conn.connection
+ super
+ end
+
+ def collect(node)
+ @visitor.accept(node, Collectors::Bind.new)
+ end
+
+ def compile(node)
+ collect(node).value
+ end
+
+ def ast_with_binds(bvs)
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.where(table[:age].eq(Nodes::BindParam.new(bvs.shift)))
+ manager.where(table[:name].eq(Nodes::BindParam.new(bvs.shift)))
+ manager.ast
+ end
+
+ def test_compile_gathers_all_bind_params
+ binds = compile(ast_with_binds(["hello", "world"]))
+ assert_equal ["hello", "world"], binds
+
+ binds = compile(ast_with_binds(["hello2", "world3"]))
+ assert_equal ["hello2", "world3"], binds
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/collectors/composite_test.rb b/activerecord/test/cases/arel/collectors/composite_test.rb
new file mode 100644
index 0000000000..545637496f
--- /dev/null
+++ b/activerecord/test/cases/arel/collectors/composite_test.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+require "arel/collectors/bind"
+require "arel/collectors/composite"
+
+module Arel
+ module Collectors
+ class TestComposite < Arel::Test
+ def setup
+ @conn = FakeRecord::Base.new
+ @visitor = Visitors::ToSql.new @conn.connection
+ super
+ end
+
+ def collect(node)
+ sql_collector = Collectors::SQLString.new
+ bind_collector = Collectors::Bind.new
+ collector = Collectors::Composite.new(sql_collector, bind_collector)
+ @visitor.accept(node, collector)
+ end
+
+ def compile(node)
+ collect(node).value
+ end
+
+ def ast_with_binds(bvs)
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.where(table[:age].eq(Nodes::BindParam.new(bvs.shift)))
+ manager.where(table[:name].eq(Nodes::BindParam.new(bvs.shift)))
+ manager.ast
+ end
+
+ def test_composite_collector_performs_multiple_collections_at_once
+ sql, binds = compile(ast_with_binds(["hello", "world"]))
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql
+ assert_equal ["hello", "world"], binds
+
+ sql, binds = compile(ast_with_binds(["hello2", "world3"]))
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql
+ assert_equal ["hello2", "world3"], binds
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/collectors/sql_string_test.rb b/activerecord/test/cases/arel/collectors/sql_string_test.rb
new file mode 100644
index 0000000000..380573494d
--- /dev/null
+++ b/activerecord/test/cases/arel/collectors/sql_string_test.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Collectors
+ class TestSqlString < Arel::Test
+ def setup
+ @conn = FakeRecord::Base.new
+ @visitor = Visitors::ToSql.new @conn.connection
+ super
+ end
+
+ def collect(node)
+ @visitor.accept(node, Collectors::SQLString.new)
+ end
+
+ def compile(node)
+ collect(node).value
+ end
+
+ def ast_with_binds(bv)
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.where(table[:age].eq(bv))
+ manager.where(table[:name].eq(bv))
+ manager.ast
+ end
+
+ def test_compile
+ bv = Nodes::BindParam.new(1)
+ collector = collect ast_with_binds bv
+
+ sql = collector.compile ["hello", "world"]
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql
+ end
+
+ def test_returned_sql_uses_utf8_encoding
+ bv = Nodes::BindParam.new(1)
+ collector = collect ast_with_binds bv
+
+ sql = collector.compile ["hello", "world"]
+ assert_equal sql.encoding, Encoding::UTF_8
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb b/activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb
new file mode 100644
index 0000000000..255c8e79e9
--- /dev/null
+++ b/activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "arel/collectors/substitute_binds"
+require "arel/collectors/sql_string"
+
+module Arel
+ module Collectors
+ class TestSubstituteBindCollector < Arel::Test
+ def setup
+ @conn = FakeRecord::Base.new
+ @visitor = Visitors::ToSql.new @conn.connection
+ super
+ end
+
+ def ast_with_binds
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.where(table[:age].eq(Nodes::BindParam.new("hello")))
+ manager.where(table[:name].eq(Nodes::BindParam.new("world")))
+ manager.ast
+ end
+
+ def compile(node, quoter)
+ collector = Collectors::SubstituteBinds.new(quoter, Collectors::SQLString.new)
+ @visitor.accept(node, collector).value
+ end
+
+ def test_compile
+ quoter = Object.new
+ def quoter.quote(val)
+ val.to_s
+ end
+ sql = compile(ast_with_binds, quoter)
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = hello AND "users"."name" = world', sql
+ end
+
+ def test_quoting_is_delegated_to_quoter
+ quoter = Object.new
+ def quoter.quote(val)
+ val.inspect
+ end
+ sql = compile(ast_with_binds, quoter)
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = "hello" AND "users"."name" = "world"', sql
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/crud_test.rb b/activerecord/test/cases/arel/crud_test.rb
new file mode 100644
index 0000000000..f3cdd8927f
--- /dev/null
+++ b/activerecord/test/cases/arel/crud_test.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class FakeCrudder < SelectManager
+ class FakeEngine
+ attr_reader :calls, :connection_pool, :spec, :config
+
+ def initialize
+ @calls = []
+ @connection_pool = self
+ @spec = self
+ @config = { adapter: "sqlite3" }
+ end
+
+ def connection; self end
+
+ def method_missing(name, *args)
+ @calls << [name, args]
+ end
+ end
+
+ include Crud
+
+ attr_reader :engine
+ attr_accessor :ctx
+
+ def initialize(engine = FakeEngine.new)
+ super
+ end
+ end
+
+ describe "crud" do
+ describe "insert" do
+ it "should call insert on the connection" do
+ table = Table.new :users
+ fc = FakeCrudder.new
+ fc.from table
+ im = fc.compile_insert [[table[:id], "foo"]]
+ assert_instance_of Arel::InsertManager, im
+ end
+ end
+
+ describe "update" do
+ it "should call update on the connection" do
+ table = Table.new :users
+ fc = FakeCrudder.new
+ fc.from table
+ stmt = fc.compile_update [[table[:id], "foo"]], Arel::Attributes::Attribute.new(table, "id")
+ assert_instance_of Arel::UpdateManager, stmt
+ end
+ end
+
+ describe "delete" do
+ it "should call delete on the connection" do
+ table = Table.new :users
+ fc = FakeCrudder.new
+ fc.from table
+ stmt = fc.compile_delete
+ assert_instance_of Arel::DeleteManager, stmt
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/delete_manager_test.rb b/activerecord/test/cases/arel/delete_manager_test.rb
new file mode 100644
index 0000000000..17a5271039
--- /dev/null
+++ b/activerecord/test/cases/arel/delete_manager_test.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class DeleteManagerTest < Arel::Spec
+ describe "new" do
+ it "takes an engine" do
+ Arel::DeleteManager.new
+ end
+ end
+
+ it "handles limit properly" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.take 10
+ dm.from table
+ assert_match(/LIMIT 10/, dm.to_sql)
+ end
+
+ describe "from" do
+ it "uses from" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.from table
+ dm.to_sql.must_be_like %{ DELETE FROM "users" }
+ end
+
+ it "chains" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.from(table).must_equal dm
+ end
+ end
+
+ describe "where" do
+ it "uses where values" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.from table
+ dm.where table[:id].eq(10)
+ dm.to_sql.must_be_like %{ DELETE FROM "users" WHERE "users"."id" = 10}
+ end
+
+ it "chains" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.where(table[:id].eq(10)).must_equal dm
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/factory_methods_test.rb b/activerecord/test/cases/arel/factory_methods_test.rb
new file mode 100644
index 0000000000..26d2cdd08d
--- /dev/null
+++ b/activerecord/test/cases/arel/factory_methods_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ module FactoryMethods
+ class TestFactoryMethods < Arel::Test
+ class Factory
+ include Arel::FactoryMethods
+ end
+
+ def setup
+ @factory = Factory.new
+ end
+
+ def test_create_join
+ join = @factory.create_join :one, :two
+ assert_kind_of Nodes::Join, join
+ assert_equal :two, join.right
+ end
+
+ def test_create_on
+ on = @factory.create_on :one
+ assert_instance_of Nodes::On, on
+ assert_equal :one, on.expr
+ end
+
+ def test_create_true
+ true_node = @factory.create_true
+ assert_instance_of Nodes::True, true_node
+ end
+
+ def test_create_false
+ false_node = @factory.create_false
+ assert_instance_of Nodes::False, false_node
+ end
+
+ def test_lower
+ lower = @factory.lower :one
+ assert_instance_of Nodes::NamedFunction, lower
+ assert_equal "LOWER", lower.name
+ assert_equal [:one], lower.expressions.map(&:expr)
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/helper.rb b/activerecord/test/cases/arel/helper.rb
new file mode 100644
index 0000000000..1f8612f799
--- /dev/null
+++ b/activerecord/test/cases/arel/helper.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "rubygems"
+require "minitest/autorun"
+require "arel"
+
+require_relative "support/fake_record"
+
+class Object
+ def must_be_like(other)
+ gsub(/\s+/, " ").strip.must_equal other.gsub(/\s+/, " ").strip
+ end
+end
+
+module Arel
+ class Test < Minitest::Test
+ def setup
+ super
+ @arel_engine = Arel::Table.engine
+ Arel::Table.engine = FakeRecord::Base.new
+ end
+
+ def teardown
+ Arel::Table.engine = @arel_engine if defined? @arel_engine
+ super
+ end
+
+ def assert_like(expected, actual)
+ assert_equal expected.gsub(/\s+/, " ").strip,
+ actual.gsub(/\s+/, " ").strip
+ end
+ end
+
+ class Spec < Minitest::Spec
+ before do
+ @arel_engine = Arel::Table.engine
+ Arel::Table.engine = FakeRecord::Base.new
+ end
+
+ after do
+ Arel::Table.engine = @arel_engine if defined? @arel_engine
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/insert_manager_test.rb b/activerecord/test/cases/arel/insert_manager_test.rb
new file mode 100644
index 0000000000..ae10ccf56c
--- /dev/null
+++ b/activerecord/test/cases/arel/insert_manager_test.rb
@@ -0,0 +1,244 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class InsertManagerTest < Arel::Spec
+ describe "new" do
+ it "takes an engine" do
+ Arel::InsertManager.new
+ end
+ end
+
+ describe "insert" do
+ it "can create a Values node" do
+ manager = Arel::InsertManager.new
+ values = manager.create_values %w{ a b }, %w{ c d }
+
+ assert_kind_of Arel::Nodes::Values, values
+ assert_equal %w{ a b }, values.left
+ assert_equal %w{ c d }, values.right
+ end
+
+ it "allows sql literals" do
+ manager = Arel::InsertManager.new
+ manager.into Table.new(:users)
+ manager.values = manager.create_values [Arel.sql("*")], %w{ a }
+ manager.to_sql.must_be_like %{
+ INSERT INTO \"users\" VALUES (*)
+ }
+ end
+
+ it "works with multiple values" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.columns << table[:id]
+ manager.columns << table[:name]
+
+ manager.values = manager.create_values_list([
+ %w{1 david},
+ %w{2 kir},
+ ["3", Arel.sql("DEFAULT")],
+ ])
+
+ manager.to_sql.must_be_like %{
+ INSERT INTO \"users\" (\"id\", \"name\") VALUES ('1', 'david'), ('2', 'kir'), ('3', DEFAULT)
+ }
+ end
+
+ it "literals in multiple values are not escaped" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.columns << table[:name]
+
+ manager.values = manager.create_values_list([
+ [Arel.sql("*")],
+ [Arel.sql("DEFAULT")],
+ ])
+
+ manager.to_sql.must_be_like %{
+ INSERT INTO \"users\" (\"name\") VALUES (*), (DEFAULT)
+ }
+ end
+
+ it "works with multiple single values" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.columns << table[:name]
+
+ manager.values = manager.create_values_list([
+ %w{david},
+ %w{kir},
+ [Arel.sql("DEFAULT")],
+ ])
+
+ manager.to_sql.must_be_like %{
+ INSERT INTO \"users\" (\"name\") VALUES ('david'), ('kir'), (DEFAULT)
+ }
+ end
+
+ it "inserts false" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+
+ manager.insert [[table[:bool], false]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("bool") VALUES ('f')
+ }
+ end
+
+ it "inserts null" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.insert [[table[:id], nil]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id") VALUES (NULL)
+ }
+ end
+
+ it "inserts time" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+
+ time = Time.now
+ attribute = table[:created_at]
+
+ manager.insert [[attribute, time]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("created_at") VALUES (#{Table.engine.connection.quote time})
+ }
+ end
+
+ it "takes a list of lists" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.into table
+ manager.insert [[table[:id], 1], [table[:name], "aaron"]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") VALUES (1, 'aaron')
+ }
+ end
+
+ it "defaults the table" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.insert [[table[:id], 1], [table[:name], "aaron"]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") VALUES (1, 'aaron')
+ }
+ end
+
+ it "noop for empty list" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.insert [[table[:id], 1]]
+ manager.insert []
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id") VALUES (1)
+ }
+ end
+
+ it "is chainable" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ insert_result = manager.insert [[table[:id], 1]]
+ assert_equal manager, insert_result
+ end
+ end
+
+ describe "into" do
+ it "takes a Table and chains" do
+ manager = Arel::InsertManager.new
+ manager.into(Table.new(:users)).must_equal manager
+ end
+
+ it "converts to sql" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users"
+ }
+ end
+ end
+
+ describe "columns" do
+ it "converts to sql" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+ manager.columns << table[:id]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id")
+ }
+ end
+ end
+
+ describe "values" do
+ it "converts to sql" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.values = Nodes::Values.new [1]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" VALUES (1)
+ }
+ end
+
+ it "accepts sql literals" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.values = Arel.sql("DEFAULT VALUES")
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" DEFAULT VALUES
+ }
+ end
+ end
+
+ describe "combo" do
+ it "combines columns and values list in order" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.values = Nodes::Values.new [1, "aaron"]
+ manager.columns << table[:id]
+ manager.columns << table[:name]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") VALUES (1, 'aaron')
+ }
+ end
+ end
+
+ describe "select" do
+
+ it "accepts a select query in place of a VALUES clause" do
+ table = Table.new :users
+
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ select = Arel::SelectManager.new
+ select.project Arel.sql("1")
+ select.project Arel.sql('"aaron"')
+
+ manager.select select
+ manager.columns << table[:id]
+ manager.columns << table[:name]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") (SELECT 1, "aaron")
+ }
+ end
+
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/and_test.rb b/activerecord/test/cases/arel/nodes/and_test.rb
new file mode 100644
index 0000000000..eff54abd91
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/and_test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "And" do
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [And.new(["foo", "bar"]), And.new(["foo", "bar"])]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [And.new(["foo", "bar"]), And.new(["foo", "baz"])]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/as_test.rb b/activerecord/test/cases/arel/nodes/as_test.rb
new file mode 100644
index 0000000000..1169ea11c9
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/as_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "As" do
+ describe "#as" do
+ it "makes an AS node" do
+ attr = Table.new(:users)[:id]
+ as = attr.as(Arel.sql("foo"))
+ assert_equal attr, as.left
+ assert_equal "foo", as.right
+ end
+
+ it "converts right to SqlLiteral if a string" do
+ attr = Table.new(:users)[:id]
+ as = attr.as("foo")
+ assert_kind_of Arel::Nodes::SqlLiteral, as.right
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [As.new("foo", "bar"), As.new("foo", "bar")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [As.new("foo", "bar"), As.new("foo", "baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/ascending_test.rb b/activerecord/test/cases/arel/nodes/ascending_test.rb
new file mode 100644
index 0000000000..1efb16222a
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/ascending_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestAscending < Arel::Test
+ def test_construct
+ ascending = Ascending.new "zomg"
+ assert_equal "zomg", ascending.expr
+ end
+
+ def test_reverse
+ ascending = Ascending.new "zomg"
+ descending = ascending.reverse
+ assert_kind_of Descending, descending
+ assert_equal ascending.expr, descending.expr
+ end
+
+ def test_direction
+ ascending = Ascending.new "zomg"
+ assert_equal :asc, ascending.direction
+ end
+
+ def test_ascending?
+ ascending = Ascending.new "zomg"
+ assert ascending.ascending?
+ end
+
+ def test_descending?
+ ascending = Ascending.new "zomg"
+ assert !ascending.descending?
+ end
+
+ def test_equality_with_same_ivars
+ array = [Ascending.new("zomg"), Ascending.new("zomg")]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [Ascending.new("zomg"), Ascending.new("zomg!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/bin_test.rb b/activerecord/test/cases/arel/nodes/bin_test.rb
new file mode 100644
index 0000000000..ee2ec3cf2f
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/bin_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestBin < Arel::Test
+ def test_new
+ assert Arel::Nodes::Bin.new("zomg")
+ end
+
+ def test_default_to_sql
+ viz = Arel::Visitors::ToSql.new Table.engine.connection_pool
+ node = Arel::Nodes::Bin.new(Arel.sql("zomg"))
+ assert_equal "zomg", viz.accept(node, Collectors::SQLString.new).value
+ end
+
+ def test_mysql_to_sql
+ viz = Arel::Visitors::MySQL.new Table.engine.connection_pool
+ node = Arel::Nodes::Bin.new(Arel.sql("zomg"))
+ assert_equal "BINARY zomg", viz.accept(node, Collectors::SQLString.new).value
+ end
+
+ def test_equality_with_same_ivars
+ array = [Bin.new("zomg"), Bin.new("zomg")]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [Bin.new("zomg"), Bin.new("zomg!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/binary_test.rb b/activerecord/test/cases/arel/nodes/binary_test.rb
new file mode 100644
index 0000000000..9bc55a155b
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/binary_test.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "Binary" do
+ describe "#hash" do
+ it "generates a hash based on its value" do
+ eq = Equality.new("foo", "bar")
+ eq2 = Equality.new("foo", "bar")
+ eq3 = Equality.new("bar", "baz")
+
+ assert_equal eq.hash, eq2.hash
+ refute_equal eq.hash, eq3.hash
+ end
+
+ it "generates a hash specific to its class" do
+ eq = Equality.new("foo", "bar")
+ neq = NotEqual.new("foo", "bar")
+
+ refute_equal eq.hash, neq.hash
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/bind_param_test.rb b/activerecord/test/cases/arel/nodes/bind_param_test.rb
new file mode 100644
index 0000000000..37a362ece4
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/bind_param_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "BindParam" do
+ it "is equal to other bind params with the same value" do
+ BindParam.new(1).must_equal(BindParam.new(1))
+ BindParam.new("foo").must_equal(BindParam.new("foo"))
+ end
+
+ it "is not equal to other nodes" do
+ BindParam.new(nil).wont_equal(Node.new)
+ end
+
+ it "is not equal to bind params with different values" do
+ BindParam.new(1).wont_equal(BindParam.new(2))
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/case_test.rb b/activerecord/test/cases/arel/nodes/case_test.rb
new file mode 100644
index 0000000000..2c087e624e
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/case_test.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "Case" do
+ describe "#initialize" do
+ it "sets case expression from first argument" do
+ node = Case.new "foo"
+
+ assert_equal "foo", node.case
+ end
+
+ it "sets default case from second argument" do
+ node = Case.new nil, "bar"
+
+ assert_equal "bar", node.default
+ end
+ end
+
+ describe "#clone" do
+ it "clones case, conditions and default" do
+ foo = Nodes.build_quoted "foo"
+
+ node = Case.new
+ node.case = foo
+ node.conditions = [When.new(foo, foo)]
+ node.default = foo
+
+ dolly = node.clone
+
+ assert_equal dolly.case, node.case
+ refute_same dolly.case, node.case
+
+ assert_equal dolly.conditions, node.conditions
+ refute_same dolly.conditions, node.conditions
+
+ assert_equal dolly.default, node.default
+ refute_same dolly.default, node.default
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ foo = Nodes.build_quoted "foo"
+ one = Nodes.build_quoted 1
+ zero = Nodes.build_quoted 0
+
+ case1 = Case.new foo
+ case1.conditions = [When.new(foo, one)]
+ case1.default = Else.new zero
+
+ case2 = Case.new foo
+ case2.conditions = [When.new(foo, one)]
+ case2.default = Else.new zero
+
+ array = [case1, case2]
+
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ foo = Nodes.build_quoted "foo"
+ bar = Nodes.build_quoted "bar"
+ one = Nodes.build_quoted 1
+ zero = Nodes.build_quoted 0
+
+ case1 = Case.new foo
+ case1.conditions = [When.new(foo, one)]
+ case1.default = Else.new zero
+
+ case2 = Case.new foo
+ case2.conditions = [When.new(bar, one)]
+ case2.default = Else.new zero
+
+ array = [case1, case2]
+
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/casted_test.rb b/activerecord/test/cases/arel/nodes/casted_test.rb
new file mode 100644
index 0000000000..e27f58a4e2
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/casted_test.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe Casted do
+ describe "#hash" do
+ it "is equal when eql? returns true" do
+ one = Casted.new 1, 2
+ also_one = Casted.new 1, 2
+
+ assert_equal one.hash, also_one.hash
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/count_test.rb b/activerecord/test/cases/arel/nodes/count_test.rb
new file mode 100644
index 0000000000..3107659e77
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/count_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+class Arel::Nodes::CountTest < Arel::Spec
+ describe "as" do
+ it "should alias the count" do
+ table = Arel::Table.new :users
+ table[:id].count.as("foo").to_sql.must_be_like %{
+ COUNT("users"."id") AS foo
+ }
+ end
+ end
+
+ describe "eq" do
+ it "should compare the count" do
+ table = Arel::Table.new :users
+ table[:id].count.eq(2).to_sql.must_be_like %{
+ COUNT("users"."id") = 2
+ }
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Arel::Nodes::Count.new("foo"), Arel::Nodes::Count.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Arel::Nodes::Count.new("foo"), Arel::Nodes::Count.new("foo!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+
+ describe "math" do
+ it "allows mathematical functions" do
+ table = Arel::Table.new :users
+ (table[:id].count + 1).to_sql.must_be_like %{
+ (COUNT("users"."id") + 1)
+ }
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/delete_statement_test.rb b/activerecord/test/cases/arel/nodes/delete_statement_test.rb
new file mode 100644
index 0000000000..3f078063a4
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/delete_statement_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+describe Arel::Nodes::DeleteStatement do
+ describe "#clone" do
+ it "clones wheres" do
+ statement = Arel::Nodes::DeleteStatement.new
+ statement.wheres = %w[a b c]
+
+ dolly = statement.clone
+ dolly.wheres.must_equal statement.wheres
+ dolly.wheres.wont_be_same_as statement.wheres
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ statement1 = Arel::Nodes::DeleteStatement.new
+ statement1.wheres = %w[a b c]
+ statement2 = Arel::Nodes::DeleteStatement.new
+ statement2.wheres = %w[a b c]
+ array = [statement1, statement2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ statement1 = Arel::Nodes::DeleteStatement.new
+ statement1.wheres = %w[a b c]
+ statement2 = Arel::Nodes::DeleteStatement.new
+ statement2.wheres = %w[1 2 3]
+ array = [statement1, statement2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/descending_test.rb b/activerecord/test/cases/arel/nodes/descending_test.rb
new file mode 100644
index 0000000000..45e22de17b
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/descending_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestDescending < Arel::Test
+ def test_construct
+ descending = Descending.new "zomg"
+ assert_equal "zomg", descending.expr
+ end
+
+ def test_reverse
+ descending = Descending.new "zomg"
+ ascending = descending.reverse
+ assert_kind_of Ascending, ascending
+ assert_equal descending.expr, ascending.expr
+ end
+
+ def test_direction
+ descending = Descending.new "zomg"
+ assert_equal :desc, descending.direction
+ end
+
+ def test_ascending?
+ descending = Descending.new "zomg"
+ assert !descending.ascending?
+ end
+
+ def test_descending?
+ descending = Descending.new "zomg"
+ assert descending.descending?
+ end
+
+ def test_equality_with_same_ivars
+ array = [Descending.new("zomg"), Descending.new("zomg")]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [Descending.new("zomg"), Descending.new("zomg!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/distinct_test.rb b/activerecord/test/cases/arel/nodes/distinct_test.rb
new file mode 100644
index 0000000000..de5f0ee588
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/distinct_test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "Distinct" do
+ describe "equality" do
+ it "is equal to other distinct nodes" do
+ array = [Distinct.new, Distinct.new]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with other nodes" do
+ array = [Distinct.new, Node.new]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/equality_test.rb b/activerecord/test/cases/arel/nodes/equality_test.rb
new file mode 100644
index 0000000000..e173720e86
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/equality_test.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "equality" do
+ # FIXME: backwards compat
+ describe "backwards compat" do
+ describe "operator" do
+ it "returns :==" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ left.operator.must_equal :==
+ end
+ end
+
+ describe "operand1" do
+ it "should equal left" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ left.left.must_equal left.operand1
+ end
+ end
+
+ describe "operand2" do
+ it "should equal right" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ left.right.must_equal left.operand2
+ end
+ end
+
+ describe "to_sql" do
+ it "takes an engine" do
+ engine = FakeRecord::Base.new
+ engine.connection.extend Module.new {
+ attr_accessor :quote_count
+ def quote(*args) @quote_count += 1; super; end
+ def quote_column_name(*args) @quote_count += 1; super; end
+ def quote_table_name(*args) @quote_count += 1; super; end
+ }
+ engine.connection.quote_count = 0
+
+ attr = Table.new(:users)[:id]
+ test = attr.eq(10)
+ test.to_sql engine
+ engine.connection.quote_count.must_equal 3
+ end
+ end
+ end
+
+ describe "or" do
+ it "makes an OR node" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ right = attr.eq(11)
+ node = left.or right
+ node.expr.left.must_equal left
+ node.expr.right.must_equal right
+ end
+ end
+
+ describe "and" do
+ it "makes and AND node" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ right = attr.eq(11)
+ node = left.and right
+ node.left.must_equal left
+ node.right.must_equal right
+ end
+ end
+
+ it "is equal with equal ivars" do
+ array = [Equality.new("foo", "bar"), Equality.new("foo", "bar")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Equality.new("foo", "bar"), Equality.new("foo", "baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/extract_test.rb b/activerecord/test/cases/arel/nodes/extract_test.rb
new file mode 100644
index 0000000000..8fc1e04d67
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/extract_test.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+class Arel::Nodes::ExtractTest < Arel::Spec
+ it "should extract field" do
+ table = Arel::Table.new :users
+ table[:timestamp].extract("date").to_sql.must_be_like %{
+ EXTRACT(DATE FROM "users"."timestamp")
+ }
+ end
+
+ describe "as" do
+ it "should alias the extract" do
+ table = Arel::Table.new :users
+ table[:timestamp].extract("date").as("foo").to_sql.must_be_like %{
+ EXTRACT(DATE FROM "users"."timestamp") AS foo
+ }
+ end
+
+ it "should not mutate the extract" do
+ table = Arel::Table.new :users
+ extract = table[:timestamp].extract("date")
+ before = extract.dup
+ extract.as("foo")
+ assert_equal extract, before
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ table = Arel::Table.new :users
+ array = [table[:attr].extract("foo"), table[:attr].extract("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ table = Arel::Table.new :users
+ array = [table[:attr].extract("foo"), table[:attr].extract("bar")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/false_test.rb b/activerecord/test/cases/arel/nodes/false_test.rb
new file mode 100644
index 0000000000..4ecf8e332e
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/false_test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "False" do
+ describe "equality" do
+ it "is equal to other false nodes" do
+ array = [False.new, False.new]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with other nodes" do
+ array = [False.new, Node.new]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/grouping_test.rb b/activerecord/test/cases/arel/nodes/grouping_test.rb
new file mode 100644
index 0000000000..03d5c142d5
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/grouping_test.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class GroupingTest < Arel::Spec
+ it "should create Equality nodes" do
+ grouping = Grouping.new(Nodes.build_quoted("foo"))
+ grouping.eq("foo").to_sql.must_be_like "('foo') = 'foo'"
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Grouping.new("foo"), Grouping.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Grouping.new("foo"), Grouping.new("bar")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/infix_operation_test.rb b/activerecord/test/cases/arel/nodes/infix_operation_test.rb
new file mode 100644
index 0000000000..dcf2200c12
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/infix_operation_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestInfixOperation < Arel::Test
+ def test_construct
+ operation = InfixOperation.new :+, 1, 2
+ assert_equal :+, operation.operator
+ assert_equal 1, operation.left
+ assert_equal 2, operation.right
+ end
+
+ def test_operation_alias
+ operation = InfixOperation.new :+, 1, 2
+ aliaz = operation.as("zomg")
+ assert_kind_of As, aliaz
+ assert_equal operation, aliaz.left
+ assert_equal "zomg", aliaz.right
+ end
+
+ def test_operation_ordering
+ operation = InfixOperation.new :+, 1, 2
+ ordering = operation.desc
+ assert_kind_of Descending, ordering
+ assert_equal operation, ordering.expr
+ assert ordering.descending?
+ end
+
+ def test_equality_with_same_ivars
+ array = [InfixOperation.new(:+, 1, 2), InfixOperation.new(:+, 1, 2)]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [InfixOperation.new(:+, 1, 2), InfixOperation.new(:+, 1, 3)]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/insert_statement_test.rb b/activerecord/test/cases/arel/nodes/insert_statement_test.rb
new file mode 100644
index 0000000000..252a0d0d0b
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/insert_statement_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+describe Arel::Nodes::InsertStatement do
+ describe "#clone" do
+ it "clones columns and values" do
+ statement = Arel::Nodes::InsertStatement.new
+ statement.columns = %w[a b c]
+ statement.values = %w[x y z]
+
+ dolly = statement.clone
+ dolly.columns.must_equal statement.columns
+ dolly.values.must_equal statement.values
+
+ dolly.columns.wont_be_same_as statement.columns
+ dolly.values.wont_be_same_as statement.values
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ statement1 = Arel::Nodes::InsertStatement.new
+ statement1.columns = %w[a b c]
+ statement1.values = %w[x y z]
+ statement2 = Arel::Nodes::InsertStatement.new
+ statement2.columns = %w[a b c]
+ statement2.values = %w[x y z]
+ array = [statement1, statement2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ statement1 = Arel::Nodes::InsertStatement.new
+ statement1.columns = %w[a b c]
+ statement1.values = %w[x y z]
+ statement2 = Arel::Nodes::InsertStatement.new
+ statement2.columns = %w[a b c]
+ statement2.values = %w[1 2 3]
+ array = [statement1, statement2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/named_function_test.rb b/activerecord/test/cases/arel/nodes/named_function_test.rb
new file mode 100644
index 0000000000..dbd7ae43be
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/named_function_test.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestNamedFunction < Arel::Test
+ def test_construct
+ function = NamedFunction.new "omg", "zomg"
+ assert_equal "omg", function.name
+ assert_equal "zomg", function.expressions
+ end
+
+ def test_function_alias
+ function = NamedFunction.new "omg", "zomg"
+ function = function.as("wth")
+ assert_equal "omg", function.name
+ assert_equal "zomg", function.expressions
+ assert_kind_of SqlLiteral, function.alias
+ assert_equal "wth", function.alias
+ end
+
+ def test_construct_with_alias
+ function = NamedFunction.new "omg", "zomg", "wth"
+ assert_equal "omg", function.name
+ assert_equal "zomg", function.expressions
+ assert_kind_of SqlLiteral, function.alias
+ assert_equal "wth", function.alias
+ end
+
+ def test_equality_with_same_ivars
+ array = [
+ NamedFunction.new("omg", "zomg", "wth"),
+ NamedFunction.new("omg", "zomg", "wth")
+ ]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [
+ NamedFunction.new("omg", "zomg", "wth"),
+ NamedFunction.new("zomg", "zomg", "wth")
+ ]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/node_test.rb b/activerecord/test/cases/arel/nodes/node_test.rb
new file mode 100644
index 0000000000..f4f07ef2c5
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/node_test.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ class TestNode < Arel::Test
+ def test_includes_factory_methods
+ assert Node.new.respond_to?(:create_join)
+ end
+
+ def test_all_nodes_are_nodes
+ Nodes.constants.map { |k|
+ Nodes.const_get(k)
+ }.grep(Class).each do |klass|
+ next if Nodes::SqlLiteral == klass
+ next if Nodes::BindParam == klass
+ next if klass.name =~ /^Arel::Nodes::(?:Test|.*Test$)/
+ assert klass.ancestors.include?(Nodes::Node), klass.name
+ end
+ end
+
+ def test_each
+ list = []
+ node = Nodes::Node.new
+ node.each { |n| list << n }
+ assert_equal [node], list
+ end
+
+ def test_generator
+ list = []
+ node = Nodes::Node.new
+ node.each.each { |n| list << n }
+ assert_equal [node], list
+ end
+
+ def test_enumerable
+ node = Nodes::Node.new
+ assert_kind_of Enumerable, node
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/not_test.rb b/activerecord/test/cases/arel/nodes/not_test.rb
new file mode 100644
index 0000000000..481e678700
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/not_test.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "not" do
+ describe "#not" do
+ it "makes a NOT node" do
+ attr = Table.new(:users)[:id]
+ expr = attr.eq(10)
+ node = expr.not
+ node.must_be_kind_of Not
+ node.expr.must_equal expr
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Not.new("foo"), Not.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Not.new("foo"), Not.new("baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/or_test.rb b/activerecord/test/cases/arel/nodes/or_test.rb
new file mode 100644
index 0000000000..93f826740d
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/or_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "or" do
+ describe "#or" do
+ it "makes an OR node" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ right = attr.eq(11)
+ node = left.or right
+ node.expr.left.must_equal left
+ node.expr.right.must_equal right
+
+ oror = node.or(right)
+ oror.expr.left.must_equal node
+ oror.expr.right.must_equal right
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Or.new("foo", "bar"), Or.new("foo", "bar")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Or.new("foo", "bar"), Or.new("foo", "baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/over_test.rb b/activerecord/test/cases/arel/nodes/over_test.rb
new file mode 100644
index 0000000000..981ec2e34b
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/over_test.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+class Arel::Nodes::OverTest < Arel::Spec
+ describe "as" do
+ it "should alias the expression" do
+ table = Arel::Table.new :users
+ table[:id].count.over.as("foo").to_sql.must_be_like %{
+ COUNT("users"."id") OVER () AS foo
+ }
+ end
+ end
+
+ describe "with literal" do
+ it "should reference the window definition by name" do
+ table = Arel::Table.new :users
+ table[:id].count.over("foo").to_sql.must_be_like %{
+ COUNT("users"."id") OVER "foo"
+ }
+ end
+ end
+
+ describe "with SQL literal" do
+ it "should reference the window definition by name" do
+ table = Arel::Table.new :users
+ table[:id].count.over(Arel.sql("foo")).to_sql.must_be_like %{
+ COUNT("users"."id") OVER foo
+ }
+ end
+ end
+
+ describe "with no expression" do
+ it "should use empty definition" do
+ table = Arel::Table.new :users
+ table[:id].count.over.to_sql.must_be_like %{
+ COUNT("users"."id") OVER ()
+ }
+ end
+ end
+
+ describe "with expression" do
+ it "should use definition in sub-expression" do
+ table = Arel::Table.new :users
+ window = Arel::Nodes::Window.new.order(table["foo"])
+ table[:id].count.over(window).to_sql.must_be_like %{
+ COUNT("users"."id") OVER (ORDER BY \"users\".\"foo\")
+ }
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [
+ Arel::Nodes::Over.new("foo", "bar"),
+ Arel::Nodes::Over.new("foo", "bar")
+ ]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [
+ Arel::Nodes::Over.new("foo", "bar"),
+ Arel::Nodes::Over.new("foo", "baz")
+ ]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/select_core_test.rb b/activerecord/test/cases/arel/nodes/select_core_test.rb
new file mode 100644
index 0000000000..1cdc7a2360
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/select_core_test.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestSelectCore < Arel::Test
+ def test_clone
+ core = Arel::Nodes::SelectCore.new
+ core.froms = %w[a b c]
+ core.projections = %w[d e f]
+ core.wheres = %w[g h i]
+
+ dolly = core.clone
+
+ assert_equal core.froms, dolly.froms
+ assert_equal core.projections, dolly.projections
+ assert_equal core.wheres, dolly.wheres
+
+ refute_same core.froms, dolly.froms
+ refute_same core.projections, dolly.projections
+ refute_same core.wheres, dolly.wheres
+ end
+
+ def test_set_quantifier
+ core = Arel::Nodes::SelectCore.new
+ core.set_quantifier = Arel::Nodes::Distinct.new
+ viz = Arel::Visitors::ToSql.new Table.engine.connection_pool
+ assert_match "DISTINCT", viz.accept(core, Collectors::SQLString.new).value
+ end
+
+ def test_equality_with_same_ivars
+ core1 = SelectCore.new
+ core1.froms = %w[a b c]
+ core1.projections = %w[d e f]
+ core1.wheres = %w[g h i]
+ core1.groups = %w[j k l]
+ core1.windows = %w[m n o]
+ core1.havings = %w[p q r]
+ core2 = SelectCore.new
+ core2.froms = %w[a b c]
+ core2.projections = %w[d e f]
+ core2.wheres = %w[g h i]
+ core2.groups = %w[j k l]
+ core2.windows = %w[m n o]
+ core2.havings = %w[p q r]
+ array = [core1, core2]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ core1 = SelectCore.new
+ core1.froms = %w[a b c]
+ core1.projections = %w[d e f]
+ core1.wheres = %w[g h i]
+ core1.groups = %w[j k l]
+ core1.windows = %w[m n o]
+ core1.havings = %w[p q r]
+ core2 = SelectCore.new
+ core2.froms = %w[a b c]
+ core2.projections = %w[d e f]
+ core2.wheres = %w[g h i]
+ core2.groups = %w[j k l]
+ core2.windows = %w[m n o]
+ core2.havings = %w[l o l]
+ array = [core1, core2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/select_statement_test.rb b/activerecord/test/cases/arel/nodes/select_statement_test.rb
new file mode 100644
index 0000000000..a91605de3e
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/select_statement_test.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+describe Arel::Nodes::SelectStatement do
+ describe "#clone" do
+ it "clones cores" do
+ statement = Arel::Nodes::SelectStatement.new %w[a b c]
+
+ dolly = statement.clone
+ dolly.cores.must_equal statement.cores
+ dolly.cores.wont_be_same_as statement.cores
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ statement1 = Arel::Nodes::SelectStatement.new %w[a b c]
+ statement1.offset = 1
+ statement1.limit = 2
+ statement1.lock = false
+ statement1.orders = %w[x y z]
+ statement1.with = "zomg"
+ statement2 = Arel::Nodes::SelectStatement.new %w[a b c]
+ statement2.offset = 1
+ statement2.limit = 2
+ statement2.lock = false
+ statement2.orders = %w[x y z]
+ statement2.with = "zomg"
+ array = [statement1, statement2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ statement1 = Arel::Nodes::SelectStatement.new %w[a b c]
+ statement1.offset = 1
+ statement1.limit = 2
+ statement1.lock = false
+ statement1.orders = %w[x y z]
+ statement1.with = "zomg"
+ statement2 = Arel::Nodes::SelectStatement.new %w[a b c]
+ statement2.offset = 1
+ statement2.limit = 2
+ statement2.lock = false
+ statement2.orders = %w[x y z]
+ statement2.with = "wth"
+ array = [statement1, statement2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/sql_literal_test.rb b/activerecord/test/cases/arel/nodes/sql_literal_test.rb
new file mode 100644
index 0000000000..3b95fed1f4
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/sql_literal_test.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "yaml"
+
+module Arel
+ module Nodes
+ class SqlLiteralTest < Arel::Spec
+ before do
+ @visitor = Visitors::ToSql.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ describe "sql" do
+ it "makes a sql literal node" do
+ sql = Arel.sql "foo"
+ sql.must_be_kind_of Arel::Nodes::SqlLiteral
+ end
+ end
+
+ describe "count" do
+ it "makes a count node" do
+ node = SqlLiteral.new("*").count
+ compile(node).must_be_like %{ COUNT(*) }
+ end
+
+ it "makes a distinct node" do
+ node = SqlLiteral.new("*").count true
+ compile(node).must_be_like %{ COUNT(DISTINCT *) }
+ end
+ end
+
+ describe "equality" do
+ it "makes an equality node" do
+ node = SqlLiteral.new("foo").eq(1)
+ compile(node).must_be_like %{ foo = 1 }
+ end
+
+ it "is equal with equal contents" do
+ array = [SqlLiteral.new("foo"), SqlLiteral.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different contents" do
+ array = [SqlLiteral.new("foo"), SqlLiteral.new("bar")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+
+ describe 'grouped "or" equality' do
+ it "makes a grouping node with an or node" do
+ node = SqlLiteral.new("foo").eq_any([1, 2])
+ compile(node).must_be_like %{ (foo = 1 OR foo = 2) }
+ end
+ end
+
+ describe 'grouped "and" equality' do
+ it "makes a grouping node with an and node" do
+ node = SqlLiteral.new("foo").eq_all([1, 2])
+ compile(node).must_be_like %{ (foo = 1 AND foo = 2) }
+ end
+ end
+
+ describe "serialization" do
+ it "serializes into YAML" do
+ yaml_literal = SqlLiteral.new("foo").to_yaml
+ assert_equal("foo", YAML.load(yaml_literal))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/sum_test.rb b/activerecord/test/cases/arel/nodes/sum_test.rb
new file mode 100644
index 0000000000..5015964951
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/sum_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+class Arel::Nodes::SumTest < Arel::Spec
+ describe "as" do
+ it "should alias the sum" do
+ table = Arel::Table.new :users
+ table[:id].sum.as("foo").to_sql.must_be_like %{
+ SUM("users"."id") AS foo
+ }
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Arel::Nodes::Sum.new("foo"), Arel::Nodes::Sum.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Arel::Nodes::Sum.new("foo"), Arel::Nodes::Sum.new("foo!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+
+ describe "order" do
+ it "should order the sum" do
+ table = Arel::Table.new :users
+ table[:id].sum.desc.to_sql.must_be_like %{
+ SUM("users"."id") DESC
+ }
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/table_alias_test.rb b/activerecord/test/cases/arel/nodes/table_alias_test.rb
new file mode 100644
index 0000000000..c661b6771e
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/table_alias_test.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "table alias" do
+ describe "equality" do
+ it "is equal with equal ivars" do
+ relation1 = Table.new(:users)
+ node1 = TableAlias.new relation1, :foo
+ relation2 = Table.new(:users)
+ node2 = TableAlias.new relation2, :foo
+ array = [node1, node2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ relation1 = Table.new(:users)
+ node1 = TableAlias.new relation1, :foo
+ relation2 = Table.new(:users)
+ node2 = TableAlias.new relation2, :bar
+ array = [node1, node2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/true_test.rb b/activerecord/test/cases/arel/nodes/true_test.rb
new file mode 100644
index 0000000000..1e85fe7d48
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/true_test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "True" do
+ describe "equality" do
+ it "is equal to other true nodes" do
+ array = [True.new, True.new]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with other nodes" do
+ array = [True.new, Node.new]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/unary_operation_test.rb b/activerecord/test/cases/arel/nodes/unary_operation_test.rb
new file mode 100644
index 0000000000..f0dd0c625c
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/unary_operation_test.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestUnaryOperation < Arel::Test
+ def test_construct
+ operation = UnaryOperation.new :-, 1
+ assert_equal :-, operation.operator
+ assert_equal 1, operation.expr
+ end
+
+ def test_operation_alias
+ operation = UnaryOperation.new :-, 1
+ aliaz = operation.as("zomg")
+ assert_kind_of As, aliaz
+ assert_equal operation, aliaz.left
+ assert_equal "zomg", aliaz.right
+ end
+
+ def test_operation_ordering
+ operation = UnaryOperation.new :-, 1
+ ordering = operation.desc
+ assert_kind_of Descending, ordering
+ assert_equal operation, ordering.expr
+ assert ordering.descending?
+ end
+
+ def test_equality_with_same_ivars
+ array = [UnaryOperation.new(:-, 1), UnaryOperation.new(:-, 1)]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [UnaryOperation.new(:-, 1), UnaryOperation.new(:-, 2)]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/update_statement_test.rb b/activerecord/test/cases/arel/nodes/update_statement_test.rb
new file mode 100644
index 0000000000..a83ce32f68
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/update_statement_test.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+describe Arel::Nodes::UpdateStatement do
+ describe "#clone" do
+ it "clones wheres and values" do
+ statement = Arel::Nodes::UpdateStatement.new
+ statement.wheres = %w[a b c]
+ statement.values = %w[x y z]
+
+ dolly = statement.clone
+ dolly.wheres.must_equal statement.wheres
+ dolly.wheres.wont_be_same_as statement.wheres
+
+ dolly.values.must_equal statement.values
+ dolly.values.wont_be_same_as statement.values
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ statement1 = Arel::Nodes::UpdateStatement.new
+ statement1.relation = "zomg"
+ statement1.wheres = 2
+ statement1.values = false
+ statement1.orders = %w[x y z]
+ statement1.limit = 42
+ statement1.key = "zomg"
+ statement2 = Arel::Nodes::UpdateStatement.new
+ statement2.relation = "zomg"
+ statement2.wheres = 2
+ statement2.values = false
+ statement2.orders = %w[x y z]
+ statement2.limit = 42
+ statement2.key = "zomg"
+ array = [statement1, statement2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ statement1 = Arel::Nodes::UpdateStatement.new
+ statement1.relation = "zomg"
+ statement1.wheres = 2
+ statement1.values = false
+ statement1.orders = %w[x y z]
+ statement1.limit = 42
+ statement1.key = "zomg"
+ statement2 = Arel::Nodes::UpdateStatement.new
+ statement2.relation = "zomg"
+ statement2.wheres = 2
+ statement2.values = false
+ statement2.orders = %w[x y z]
+ statement2.limit = 42
+ statement2.key = "wth"
+ array = [statement1, statement2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/window_test.rb b/activerecord/test/cases/arel/nodes/window_test.rb
new file mode 100644
index 0000000000..729b0556a4
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/window_test.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "Window" do
+ describe "equality" do
+ it "is equal with equal ivars" do
+ window1 = Window.new
+ window1.orders = [1, 2]
+ window1.partitions = [1]
+ window1.frame 3
+ window2 = Window.new
+ window2.orders = [1, 2]
+ window2.partitions = [1]
+ window2.frame 3
+ array = [window1, window2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ window1 = Window.new
+ window1.orders = [1, 2]
+ window1.partitions = [1]
+ window1.frame 3
+ window2 = Window.new
+ window2.orders = [1, 2]
+ window1.partitions = [1]
+ window2.frame 4
+ array = [window1, window2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+
+ describe "NamedWindow" do
+ describe "equality" do
+ it "is equal with equal ivars" do
+ window1 = NamedWindow.new "foo"
+ window1.orders = [1, 2]
+ window1.partitions = [1]
+ window1.frame 3
+ window2 = NamedWindow.new "foo"
+ window2.orders = [1, 2]
+ window2.partitions = [1]
+ window2.frame 3
+ array = [window1, window2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ window1 = NamedWindow.new "foo"
+ window1.orders = [1, 2]
+ window1.partitions = [1]
+ window1.frame 3
+ window2 = NamedWindow.new "bar"
+ window2.orders = [1, 2]
+ window2.partitions = [1]
+ window2.frame 3
+ array = [window1, window2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+
+ describe "CurrentRow" do
+ describe "equality" do
+ it "is equal to other current row nodes" do
+ array = [CurrentRow.new, CurrentRow.new]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with other nodes" do
+ array = [CurrentRow.new, Node.new]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes_test.rb b/activerecord/test/cases/arel/nodes_test.rb
new file mode 100644
index 0000000000..9021de0d20
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ module Nodes
+ class TestNodes < Arel::Test
+ def test_every_arel_nodes_have_hash_eql_eqeq_from_same_class
+ # #descendants code from activesupport
+ node_descendants = []
+ ObjectSpace.each_object(Arel::Nodes::Node.singleton_class) do |k|
+ next if k.respond_to?(:singleton_class?) && k.singleton_class?
+ node_descendants.unshift k unless k == self
+ end
+ node_descendants.delete(Arel::Nodes::Node)
+ node_descendants.delete(Arel::Nodes::NodeExpression)
+
+ bad_node_descendants = node_descendants.reject do |subnode|
+ eqeq_owner = subnode.instance_method(:==).owner
+ eql_owner = subnode.instance_method(:eql?).owner
+ hash_owner = subnode.instance_method(:hash).owner
+
+ eqeq_owner < Arel::Nodes::Node &&
+ eqeq_owner == eql_owner &&
+ eqeq_owner == hash_owner
+ end
+
+ problem_msg = "Some subclasses of Arel::Nodes::Node do not have a" \
+ " #== or #eql? or #hash defined from the same class as the others"
+ assert_empty bad_node_descendants, problem_msg
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/select_manager_test.rb b/activerecord/test/cases/arel/select_manager_test.rb
new file mode 100644
index 0000000000..1bc9f6abf2
--- /dev/null
+++ b/activerecord/test/cases/arel/select_manager_test.rb
@@ -0,0 +1,1236 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class SelectManagerTest < Arel::Spec
+ def test_join_sources
+ manager = Arel::SelectManager.new
+ manager.join_sources << Arel::Nodes::StringJoin.new(Nodes.build_quoted("foo"))
+ assert_equal "SELECT FROM 'foo'", manager.to_sql
+ end
+
+ describe "backwards compatibility" do
+ describe "project" do
+ it "accepts symbols as sql literals" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project :id
+ manager.from table
+ manager.to_sql.must_be_like %{
+ SELECT id FROM "users"
+ }
+ end
+ end
+
+ describe "order" do
+ it "accepts symbols" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.from table
+ manager.order :foo
+ manager.to_sql.must_be_like %{ SELECT * FROM "users" ORDER BY foo }
+ end
+ end
+
+ describe "group" do
+ it "takes a symbol" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.group :foo
+ manager.to_sql.must_be_like %{ SELECT FROM "users" GROUP BY foo }
+ end
+ end
+
+ describe "as" do
+ it "makes an AS node by grouping the AST" do
+ manager = Arel::SelectManager.new
+ as = manager.as(Arel.sql("foo"))
+ assert_kind_of Arel::Nodes::Grouping, as.left
+ assert_equal manager.ast, as.left.expr
+ assert_equal "foo", as.right
+ end
+
+ it "converts right to SqlLiteral if a string" do
+ manager = Arel::SelectManager.new
+ as = manager.as("foo")
+ assert_kind_of Arel::Nodes::SqlLiteral, as.right
+ end
+
+ it "can make a subselect" do
+ manager = Arel::SelectManager.new
+ manager.project Arel.star
+ manager.from Arel.sql("zomg")
+ as = manager.as(Arel.sql("foo"))
+
+ manager = Arel::SelectManager.new
+ manager.project Arel.sql("name")
+ manager.from as
+ manager.to_sql.must_be_like "SELECT name FROM (SELECT * FROM zomg) foo"
+ end
+ end
+
+ describe "from" do
+ it "ignores strings when table of same name exists" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+
+ manager.from table
+ manager.from "users"
+ manager.project table["id"]
+ manager.to_sql.must_be_like 'SELECT "users"."id" FROM users'
+ end
+
+ it "should support any ast" do
+ table = Table.new :users
+ manager1 = Arel::SelectManager.new
+
+ manager2 = Arel::SelectManager.new
+ manager2.project(Arel.sql("*"))
+ manager2.from table
+
+ manager1.project Arel.sql("lol")
+ as = manager2.as Arel.sql("omg")
+ manager1.from(as)
+
+ manager1.to_sql.must_be_like %{
+ SELECT lol FROM (SELECT * FROM "users") omg
+ }
+ end
+ end
+
+ describe "having" do
+ it "converts strings to SQLLiterals" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.having Arel.sql("foo")
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" HAVING foo }
+ end
+
+ it "can have multiple items specified separately" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.having Arel.sql("foo")
+ mgr.having Arel.sql("bar")
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" HAVING foo AND bar }
+ end
+
+ it "can receive any node" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.having Arel::Nodes::And.new([Arel.sql("foo"), Arel.sql("bar")])
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" HAVING foo AND bar }
+ end
+ end
+
+ describe "on" do
+ it "converts to sqlliterals" do
+ table = Table.new :users
+ right = table.alias
+ mgr = table.from
+ mgr.join(right).on("omg")
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg }
+ end
+
+ it "converts to sqlliterals with multiple items" do
+ table = Table.new :users
+ right = table.alias
+ mgr = table.from
+ mgr.join(right).on("omg", "123")
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg AND 123 }
+ end
+ end
+ end
+
+ describe "clone" do
+ it "creates new cores" do
+ table = Table.new :users, as: "foo"
+ mgr = table.from
+ m2 = mgr.clone
+ m2.project "foo"
+ mgr.to_sql.wont_equal m2.to_sql
+ end
+
+ it "makes updates to the correct copy" do
+ table = Table.new :users, as: "foo"
+ mgr = table.from
+ m2 = mgr.clone
+ m3 = m2.clone
+ m2.project "foo"
+ mgr.to_sql.wont_equal m2.to_sql
+ m3.to_sql.must_equal mgr.to_sql
+ end
+ end
+
+ describe "initialize" do
+ it "uses alias in sql" do
+ table = Table.new :users, as: "foo"
+ mgr = table.from
+ mgr.skip 10
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" "foo" OFFSET 10 }
+ end
+ end
+
+ describe "skip" do
+ it "should add an offset" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.skip 10
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 }
+ end
+
+ it "should chain" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.skip(10).to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 }
+ end
+ end
+
+ describe "offset" do
+ it "should add an offset" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.offset = 10
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 }
+ end
+
+ it "should remove an offset" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.offset = 10
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 }
+
+ mgr.offset = nil
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" }
+ end
+
+ it "should return the offset" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.offset = 10
+ assert_equal 10, mgr.offset
+ end
+ end
+
+ describe "exists" do
+ it "should create an exists clause" do
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.project Nodes::SqlLiteral.new "*"
+ m2 = Arel::SelectManager.new
+ m2.project manager.exists
+ m2.to_sql.must_be_like %{ SELECT EXISTS (#{manager.to_sql}) }
+ end
+
+ it "can be aliased" do
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.project Nodes::SqlLiteral.new "*"
+ m2 = Arel::SelectManager.new
+ m2.project manager.exists.as("foo")
+ m2.to_sql.must_be_like %{ SELECT EXISTS (#{manager.to_sql}) AS foo }
+ end
+ end
+
+ describe "union" do
+ before do
+ table = Table.new :users
+ @m1 = Arel::SelectManager.new table
+ @m1.project Arel.star
+ @m1.where(table[:age].lt(18))
+
+ @m2 = Arel::SelectManager.new table
+ @m2.project Arel.star
+ @m2.where(table[:age].gt(99))
+
+
+ end
+
+ it "should union two managers" do
+ # FIXME should this union "managers" or "statements" ?
+ # FIXME this probably shouldn't return a node
+ node = @m1.union @m2
+
+ # maybe FIXME: decide when wrapper parens are needed
+ node.to_sql.must_be_like %{
+ ( SELECT * FROM "users" WHERE "users"."age" < 18 UNION SELECT * FROM "users" WHERE "users"."age" > 99 )
+ }
+ end
+
+ it "should union all" do
+ node = @m1.union :all, @m2
+
+ node.to_sql.must_be_like %{
+ ( SELECT * FROM "users" WHERE "users"."age" < 18 UNION ALL SELECT * FROM "users" WHERE "users"."age" > 99 )
+ }
+ end
+
+ end
+
+ describe "intersect" do
+ before do
+ table = Table.new :users
+ @m1 = Arel::SelectManager.new table
+ @m1.project Arel.star
+ @m1.where(table[:age].gt(18))
+
+ @m2 = Arel::SelectManager.new table
+ @m2.project Arel.star
+ @m2.where(table[:age].lt(99))
+
+
+ end
+
+ it "should interect two managers" do
+ # FIXME should this intersect "managers" or "statements" ?
+ # FIXME this probably shouldn't return a node
+ node = @m1.intersect @m2
+
+ # maybe FIXME: decide when wrapper parens are needed
+ node.to_sql.must_be_like %{
+ ( SELECT * FROM "users" WHERE "users"."age" > 18 INTERSECT SELECT * FROM "users" WHERE "users"."age" < 99 )
+ }
+ end
+
+ end
+
+ describe "except" do
+ before do
+ table = Table.new :users
+ @m1 = Arel::SelectManager.new table
+ @m1.project Arel.star
+ @m1.where(table[:age].between(18..60))
+
+ @m2 = Arel::SelectManager.new table
+ @m2.project Arel.star
+ @m2.where(table[:age].between(40..99))
+ end
+
+ it "should except two managers" do
+ # FIXME should this except "managers" or "statements" ?
+ # FIXME this probably shouldn't return a node
+ node = @m1.except @m2
+
+ # maybe FIXME: decide when wrapper parens are needed
+ node.to_sql.must_be_like %{
+ ( SELECT * FROM "users" WHERE "users"."age" BETWEEN 18 AND 60 EXCEPT SELECT * FROM "users" WHERE "users"."age" BETWEEN 40 AND 99 )
+ }
+ end
+
+ end
+
+ describe "with" do
+ it "should support basic WITH" do
+ users = Table.new(:users)
+ users_top = Table.new(:users_top)
+ comments = Table.new(:comments)
+
+ top = users.project(users[:id]).where(users[:karma].gt(100))
+ users_as = Arel::Nodes::As.new(users_top, top)
+ select_manager = comments.project(Arel.star).with(users_as)
+ .where(comments[:author_id].in(users_top.project(users_top[:id])))
+
+ select_manager.to_sql.must_be_like %{
+ WITH "users_top" AS (SELECT "users"."id" FROM "users" WHERE "users"."karma" > 100) SELECT * FROM "comments" WHERE "comments"."author_id" IN (SELECT "users_top"."id" FROM "users_top")
+ }
+ end
+
+ it "should support WITH RECURSIVE" do
+ comments = Table.new(:comments)
+ comments_id = comments[:id]
+ comments_parent_id = comments[:parent_id]
+
+ replies = Table.new(:replies)
+ replies_id = replies[:id]
+
+ recursive_term = Arel::SelectManager.new
+ recursive_term.from(comments).project(comments_id, comments_parent_id).where(comments_id.eq 42)
+
+ non_recursive_term = Arel::SelectManager.new
+ non_recursive_term.from(comments).project(comments_id, comments_parent_id).join(replies).on(comments_parent_id.eq replies_id)
+
+ union = recursive_term.union(non_recursive_term)
+
+ as_statement = Arel::Nodes::As.new replies, union
+
+ manager = Arel::SelectManager.new
+ manager.with(:recursive, as_statement).from(replies).project(Arel.star)
+
+ sql = manager.to_sql
+ sql.must_be_like %{
+ WITH RECURSIVE "replies" AS (
+ SELECT "comments"."id", "comments"."parent_id" FROM "comments" WHERE "comments"."id" = 42
+ UNION
+ SELECT "comments"."id", "comments"."parent_id" FROM "comments" INNER JOIN "replies" ON "comments"."parent_id" = "replies"."id"
+ )
+ SELECT * FROM "replies"
+ }
+ end
+ end
+
+ describe "ast" do
+ it "should return the ast" do
+ table = Table.new :users
+ mgr = table.from
+ assert mgr.ast
+ end
+
+ it "should allow orders to work when the ast is grepped" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.project Arel.sql "*"
+ mgr.from table
+ mgr.orders << Arel::Nodes::Ascending.new(Arel.sql("foo"))
+ mgr.ast.grep(Arel::Nodes::OuterJoin)
+ mgr.to_sql.must_be_like %{ SELECT * FROM "users" ORDER BY foo ASC }
+ end
+ end
+
+ describe "taken" do
+ it "should return limit" do
+ manager = Arel::SelectManager.new
+ manager.take 10
+ manager.taken.must_equal 10
+ end
+ end
+
+ describe "lock" do
+ # This should fail on other databases
+ it "adds a lock node" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.lock.to_sql.must_be_like %{ SELECT FROM "users" FOR UPDATE }
+ end
+ end
+
+ describe "orders" do
+ it "returns order clauses" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ order = table[:id]
+ manager.order table[:id]
+ manager.orders.must_equal [order]
+ end
+ end
+
+ describe "order" do
+ it "generates order clauses" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.from table
+ manager.order table[:id]
+ manager.to_sql.must_be_like %{
+ SELECT * FROM "users" ORDER BY "users"."id"
+ }
+ end
+
+ # FIXME: I would like to deprecate this
+ it "takes *args" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.from table
+ manager.order table[:id], table[:name]
+ manager.to_sql.must_be_like %{
+ SELECT * FROM "users" ORDER BY "users"."id", "users"."name"
+ }
+ end
+
+ it "chains" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.order(table[:id]).must_equal manager
+ end
+
+ it "has order attributes" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.from table
+ manager.order table[:id].desc
+ manager.to_sql.must_be_like %{
+ SELECT * FROM "users" ORDER BY "users"."id" DESC
+ }
+ end
+ end
+
+ describe "on" do
+ it "takes two params" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right).on(predicate, predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ INNER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id" AND
+ "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "takes three params" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right).on(
+ predicate,
+ predicate,
+ left[:name].eq(right[:name])
+ )
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ INNER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id" AND
+ "users"."id" = "users_2"."id" AND
+ "users"."name" = "users_2"."name"
+ }
+ end
+ end
+
+ it "should hand back froms" do
+ relation = Arel::SelectManager.new
+ assert_equal [], relation.froms
+ end
+
+ it "should create and nodes" do
+ relation = Arel::SelectManager.new
+ children = ["foo", "bar", "baz"]
+ clause = relation.create_and children
+ assert_kind_of Arel::Nodes::And, clause
+ assert_equal children, clause.children
+ end
+
+ it "should create insert managers" do
+ relation = Arel::SelectManager.new
+ insert = relation.create_insert
+ assert_kind_of Arel::InsertManager, insert
+ end
+
+ it "should create join nodes" do
+ relation = Arel::SelectManager.new
+ join = relation.create_join "foo", "bar"
+ assert_kind_of Arel::Nodes::InnerJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a full outer join klass" do
+ relation = Arel::SelectManager.new
+ join = relation.create_join "foo", "bar", Arel::Nodes::FullOuterJoin
+ assert_kind_of Arel::Nodes::FullOuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a outer join klass" do
+ relation = Arel::SelectManager.new
+ join = relation.create_join "foo", "bar", Arel::Nodes::OuterJoin
+ assert_kind_of Arel::Nodes::OuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a right outer join klass" do
+ relation = Arel::SelectManager.new
+ join = relation.create_join "foo", "bar", Arel::Nodes::RightOuterJoin
+ assert_kind_of Arel::Nodes::RightOuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ describe "join" do
+ it "responds to join" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ INNER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "takes a class" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right, Nodes::OuterJoin).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ LEFT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "takes the full outer join class" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right, Nodes::FullOuterJoin).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ FULL OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "takes the right outer join class" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right, Nodes::RightOuterJoin).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ RIGHT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "noops on nil" do
+ manager = Arel::SelectManager.new
+ manager.join(nil).must_equal manager
+ end
+
+ it "raises EmptyJoinError on empty" do
+ left = Table.new :users
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ assert_raises(EmptyJoinError) do
+ manager.join("")
+ end
+ end
+ end
+
+ describe "outer join" do
+ it "responds to join" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.outer_join(right).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ LEFT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "noops on nil" do
+ manager = Arel::SelectManager.new
+ manager.outer_join(nil).must_equal manager
+ end
+ end
+
+ describe "joins" do
+
+ it "returns inner join sql" do
+ table = Table.new :users
+ aliaz = table.alias
+ manager = Arel::SelectManager.new
+ manager.from Nodes::InnerJoin.new(aliaz, table[:id].eq(aliaz[:id]))
+ assert_match 'INNER JOIN "users" "users_2" "users"."id" = "users_2"."id"',
+ manager.to_sql
+ end
+
+ it "returns outer join sql" do
+ table = Table.new :users
+ aliaz = table.alias
+ manager = Arel::SelectManager.new
+ manager.from Nodes::OuterJoin.new(aliaz, table[:id].eq(aliaz[:id]))
+ assert_match 'LEFT OUTER JOIN "users" "users_2" "users"."id" = "users_2"."id"',
+ manager.to_sql
+ end
+
+ it "can have a non-table alias as relation name" do
+ users = Table.new :users
+ comments = Table.new :comments
+
+ counts = comments.from.
+ group(comments[:user_id]).
+ project(
+ comments[:user_id].as("user_id"),
+ comments[:user_id].count.as("count")
+ ).as("counts")
+
+ joins = users.join(counts).on(counts[:user_id].eq(10))
+ joins.to_sql.must_be_like %{
+ SELECT FROM "users" INNER JOIN (SELECT "comments"."user_id" AS user_id, COUNT("comments"."user_id") AS count FROM "comments" GROUP BY "comments"."user_id") counts ON counts."user_id" = 10
+ }
+ end
+
+ it "joins itself" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+
+ mgr = left.join(right)
+ mgr.project Nodes::SqlLiteral.new("*")
+ mgr.on(predicate).must_equal mgr
+
+ mgr.to_sql.must_be_like %{
+ SELECT * FROM "users"
+ INNER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "returns string join sql" do
+ manager = Arel::SelectManager.new
+ manager.from Nodes::StringJoin.new(Nodes.build_quoted("hello"))
+ assert_match "'hello'", manager.to_sql
+ end
+ end
+
+ describe "group" do
+ it "takes an attribute" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.group table[:id]
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" GROUP BY "users"."id"
+ }
+ end
+
+ it "chains" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.group(table[:id]).must_equal manager
+ end
+
+ it "takes multiple args" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.group table[:id], table[:name]
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" GROUP BY "users"."id", "users"."name"
+ }
+ end
+
+ # FIXME: backwards compat
+ it "makes strings literals" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.group "foo"
+ manager.to_sql.must_be_like %{ SELECT FROM "users" GROUP BY foo }
+ end
+ end
+
+ describe "window definition" do
+ it "can be empty" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window")
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS ()
+ }
+ end
+
+ it "takes an order" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").order(table["foo"].asc)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ORDER BY "users"."foo" ASC)
+ }
+ end
+
+ it "takes an order with multiple columns" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").order(table["foo"].asc, table["bar"].desc)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ORDER BY "users"."foo" ASC, "users"."bar" DESC)
+ }
+ end
+
+ it "takes a partition" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").partition(table["bar"])
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (PARTITION BY "users"."bar")
+ }
+ end
+
+ it "takes a partition and an order" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").partition(table["foo"]).order(table["foo"].asc)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (PARTITION BY "users"."foo"
+ ORDER BY "users"."foo" ASC)
+ }
+ end
+
+ it "takes a partition with multiple columns" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").partition(table["bar"], table["baz"])
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (PARTITION BY "users"."bar", "users"."baz")
+ }
+ end
+
+ it "takes a rows frame, unbounded preceding" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::Preceding.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS UNBOUNDED PRECEDING)
+ }
+ end
+
+ it "takes a rows frame, bounded preceding" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::Preceding.new(5))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS 5 PRECEDING)
+ }
+ end
+
+ it "takes a rows frame, unbounded following" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::Following.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS UNBOUNDED FOLLOWING)
+ }
+ end
+
+ it "takes a rows frame, bounded following" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::Following.new(5))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS 5 FOLLOWING)
+ }
+ end
+
+ it "takes a rows frame, current row" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::CurrentRow.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS CURRENT ROW)
+ }
+ end
+
+ it "takes a rows frame, between two delimiters" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ window = manager.window("a_window")
+ window.frame(
+ Arel::Nodes::Between.new(
+ window.rows,
+ Nodes::And.new([
+ Arel::Nodes::Preceding.new,
+ Arel::Nodes::CurrentRow.new
+ ])))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
+ }
+ end
+
+ it "takes a range frame, unbounded preceding" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::Preceding.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE UNBOUNDED PRECEDING)
+ }
+ end
+
+ it "takes a range frame, bounded preceding" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::Preceding.new(5))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE 5 PRECEDING)
+ }
+ end
+
+ it "takes a range frame, unbounded following" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::Following.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE UNBOUNDED FOLLOWING)
+ }
+ end
+
+ it "takes a range frame, bounded following" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::Following.new(5))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE 5 FOLLOWING)
+ }
+ end
+
+ it "takes a range frame, current row" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::CurrentRow.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE CURRENT ROW)
+ }
+ end
+
+ it "takes a range frame, between two delimiters" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ window = manager.window("a_window")
+ window.frame(
+ Arel::Nodes::Between.new(
+ window.range,
+ Nodes::And.new([
+ Arel::Nodes::Preceding.new,
+ Arel::Nodes::CurrentRow.new
+ ])))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
+ }
+ end
+ end
+
+ describe "delete" do
+ it "copies from" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ stmt = manager.compile_delete
+
+ stmt.to_sql.must_be_like %{ DELETE FROM "users" }
+ end
+
+ it "copies where" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where table[:id].eq 10
+ stmt = manager.compile_delete
+
+ stmt.to_sql.must_be_like %{
+ DELETE FROM "users" WHERE "users"."id" = 10
+ }
+ end
+ end
+
+ describe "where_sql" do
+ it "gives me back the where sql" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where table[:id].eq 10
+ manager.where_sql.must_be_like %{ WHERE "users"."id" = 10 }
+ end
+
+ it "joins wheres with AND" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where table[:id].eq 10
+ manager.where table[:id].eq 11
+ manager.where_sql.must_be_like %{ WHERE "users"."id" = 10 AND "users"."id" = 11}
+ end
+
+ it "handles database specific statements" do
+ old_visitor = Table.engine.connection.visitor
+ Table.engine.connection.visitor = Visitors::PostgreSQL.new Table.engine.connection
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where table[:id].eq 10
+ manager.where table[:name].matches "foo%"
+ manager.where_sql.must_be_like %{ WHERE "users"."id" = 10 AND "users"."name" ILIKE 'foo%' }
+ Table.engine.connection.visitor = old_visitor
+ end
+
+ it "returns nil when there are no wheres" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where_sql.must_be_nil
+ end
+ end
+
+ describe "update" do
+
+ it "creates an update statement" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ stmt = manager.compile_update({ table[:id] => 1 }, Arel::Attributes::Attribute.new(table, "id"))
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET "id" = 1
+ }
+ end
+
+ it "takes a string" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ stmt = manager.compile_update(Nodes::SqlLiteral.new("foo = bar"), Arel::Attributes::Attribute.new(table, "id"))
+
+ stmt.to_sql.must_be_like %{ UPDATE "users" SET foo = bar }
+ end
+
+ it "copies limits" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.take 1
+ stmt = manager.compile_update(Nodes::SqlLiteral.new("foo = bar"), Arel::Attributes::Attribute.new(table, "id"))
+ stmt.key = table["id"]
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET foo = bar
+ WHERE "users"."id" IN (SELECT "users"."id" FROM "users" LIMIT 1)
+ }
+ end
+
+ it "copies order" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.order :foo
+ stmt = manager.compile_update(Nodes::SqlLiteral.new("foo = bar"), Arel::Attributes::Attribute.new(table, "id"))
+ stmt.key = table["id"]
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET foo = bar
+ WHERE "users"."id" IN (SELECT "users"."id" FROM "users" ORDER BY foo)
+ }
+ end
+
+ it "copies where clauses" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.where table[:id].eq 10
+ manager.from table
+ stmt = manager.compile_update({ table[:id] => 1 }, Arel::Attributes::Attribute.new(table, "id"))
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET "id" = 1 WHERE "users"."id" = 10
+ }
+ end
+
+ it "copies where clauses when nesting is triggered" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.where table[:foo].eq 10
+ manager.take 42
+ manager.from table
+ stmt = manager.compile_update({ table[:id] => 1 }, Arel::Attributes::Attribute.new(table, "id"))
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET "id" = 1 WHERE "users"."id" IN (SELECT "users"."id" FROM "users" WHERE "users"."foo" = 10 LIMIT 42)
+ }
+ end
+
+ end
+
+ describe "project" do
+ it "takes sql literals" do
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.to_sql.must_be_like %{ SELECT * }
+ end
+
+ it "takes multiple args" do
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new("foo"),
+ Nodes::SqlLiteral.new("bar")
+ manager.to_sql.must_be_like %{ SELECT foo, bar }
+ end
+
+ it "takes strings" do
+ manager = Arel::SelectManager.new
+ manager.project "*"
+ manager.to_sql.must_be_like %{ SELECT * }
+ end
+
+ end
+
+ describe "projections" do
+ it "reads projections" do
+ manager = Arel::SelectManager.new
+ manager.project Arel.sql("foo"), Arel.sql("bar")
+ manager.projections.must_equal [Arel.sql("foo"), Arel.sql("bar")]
+ end
+ end
+
+ describe "projections=" do
+ it "overwrites projections" do
+ manager = Arel::SelectManager.new
+ manager.project Arel.sql("foo")
+ manager.projections = [Arel.sql("bar")]
+ manager.to_sql.must_be_like %{ SELECT bar }
+ end
+ end
+
+ describe "take" do
+ it "knows take" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from(table).project(table["id"])
+ manager.where(table["id"].eq(1))
+ manager.take 1
+
+ manager.to_sql.must_be_like %{
+ SELECT "users"."id"
+ FROM "users"
+ WHERE "users"."id" = 1
+ LIMIT 1
+ }
+ end
+
+ it "chains" do
+ manager = Arel::SelectManager.new
+ manager.take(1).must_equal manager
+ end
+
+ it "removes LIMIT when nil is passed" do
+ manager = Arel::SelectManager.new
+ manager.limit = 10
+ assert_match("LIMIT", manager.to_sql)
+
+ manager.limit = nil
+ refute_match("LIMIT", manager.to_sql)
+ end
+ end
+
+ describe "where" do
+ it "knows where" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from(table).project(table["id"])
+ manager.where(table["id"].eq(1))
+ manager.to_sql.must_be_like %{
+ SELECT "users"."id"
+ FROM "users"
+ WHERE "users"."id" = 1
+ }
+ end
+
+ it "chains" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from(table)
+ manager.project(table["id"]).where(table["id"].eq 1).must_equal manager
+ end
+ end
+
+ describe "from" do
+ it "makes sql" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+
+ manager.from table
+ manager.project table["id"]
+ manager.to_sql.must_be_like 'SELECT "users"."id" FROM "users"'
+ end
+
+ it "chains" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from(table).project(table["id"]).must_equal manager
+ manager.to_sql.must_be_like 'SELECT "users"."id" FROM "users"'
+ end
+ end
+
+ describe "source" do
+ it "returns the join source of the select core" do
+ manager = Arel::SelectManager.new
+ manager.source.must_equal manager.ast.cores.last.source
+ end
+ end
+
+ describe "distinct" do
+ it "sets the quantifier" do
+ manager = Arel::SelectManager.new
+
+ manager.distinct
+ manager.ast.cores.last.set_quantifier.class.must_equal Arel::Nodes::Distinct
+
+ manager.distinct(false)
+ manager.ast.cores.last.set_quantifier.must_be_nil
+ end
+
+ it "chains" do
+ manager = Arel::SelectManager.new
+ manager.distinct.must_equal manager
+ manager.distinct(false).must_equal manager
+ end
+ end
+
+ describe "distinct_on" do
+ it "sets the quantifier" do
+ manager = Arel::SelectManager.new
+ table = Table.new :users
+
+ manager.distinct_on(table["id"])
+ manager.ast.cores.last.set_quantifier.must_equal Arel::Nodes::DistinctOn.new(table["id"])
+
+ manager.distinct_on(false)
+ manager.ast.cores.last.set_quantifier.must_be_nil
+ end
+
+ it "chains" do
+ manager = Arel::SelectManager.new
+ table = Table.new :users
+
+ manager.distinct_on(table["id"]).must_equal manager
+ manager.distinct_on(false).must_equal manager
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/support/fake_record.rb b/activerecord/test/cases/arel/support/fake_record.rb
new file mode 100644
index 0000000000..8620d6fd34
--- /dev/null
+++ b/activerecord/test/cases/arel/support/fake_record.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+require "date"
+module FakeRecord
+ class Column < Struct.new(:name, :type)
+ end
+
+ class Connection
+ attr_reader :tables
+ attr_accessor :visitor
+
+ def initialize(visitor = nil)
+ @tables = %w{ users photos developers products}
+ @columns = {
+ "users" => [
+ Column.new("id", :integer),
+ Column.new("name", :string),
+ Column.new("bool", :boolean),
+ Column.new("created_at", :date)
+ ],
+ "products" => [
+ Column.new("id", :integer),
+ Column.new("price", :decimal)
+ ]
+ }
+ @columns_hash = {
+ "users" => Hash[@columns["users"].map { |x| [x.name, x] }],
+ "products" => Hash[@columns["products"].map { |x| [x.name, x] }]
+ }
+ @primary_keys = {
+ "users" => "id",
+ "products" => "id"
+ }
+ @visitor = visitor
+ end
+
+ def columns_hash(table_name)
+ @columns_hash[table_name]
+ end
+
+ def primary_key(name)
+ @primary_keys[name.to_s]
+ end
+
+ def data_source_exists?(name)
+ @tables.include? name.to_s
+ end
+
+ def columns(name, message = nil)
+ @columns[name.to_s]
+ end
+
+ def quote_table_name(name)
+ "\"#{name.to_s}\""
+ end
+
+ def quote_column_name(name)
+ "\"#{name.to_s}\""
+ end
+
+ def schema_cache
+ self
+ end
+
+ def quote(thing)
+ case thing
+ when DateTime
+ "'#{thing.strftime("%Y-%m-%d %H:%M:%S")}'"
+ when Date
+ "'#{thing.strftime("%Y-%m-%d")}'"
+ when true
+ "'t'"
+ when false
+ "'f'"
+ when nil
+ "NULL"
+ when Numeric
+ thing
+ else
+ "'#{thing.to_s.gsub("'", "\\\\'")}'"
+ end
+ end
+ end
+
+ class ConnectionPool
+ class Spec < Struct.new(:config)
+ end
+
+ attr_reader :spec, :connection
+
+ def initialize
+ @spec = Spec.new(adapter: "america")
+ @connection = Connection.new
+ @connection.visitor = Arel::Visitors::ToSql.new(connection)
+ end
+
+ def with_connection
+ yield connection
+ end
+
+ def table_exists?(name)
+ connection.tables.include? name.to_s
+ end
+
+ def columns_hash
+ connection.columns_hash
+ end
+
+ def schema_cache
+ connection
+ end
+
+ def quote(thing)
+ connection.quote thing
+ end
+ end
+
+ class Base
+ attr_accessor :connection_pool
+
+ def initialize
+ @connection_pool = ConnectionPool.new
+ end
+
+ def connection
+ connection_pool.connection
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/table_test.rb b/activerecord/test/cases/arel/table_test.rb
new file mode 100644
index 0000000000..91b7a5a480
--- /dev/null
+++ b/activerecord/test/cases/arel/table_test.rb
@@ -0,0 +1,216 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class TableTest < Arel::Spec
+ before do
+ @relation = Table.new(:users)
+ end
+
+ it "should create join nodes" do
+ join = @relation.create_string_join "foo"
+ assert_kind_of Arel::Nodes::StringJoin, join
+ assert_equal "foo", join.left
+ end
+
+ it "should create join nodes" do
+ join = @relation.create_join "foo", "bar"
+ assert_kind_of Arel::Nodes::InnerJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a klass" do
+ join = @relation.create_join "foo", "bar", Arel::Nodes::FullOuterJoin
+ assert_kind_of Arel::Nodes::FullOuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a klass" do
+ join = @relation.create_join "foo", "bar", Arel::Nodes::OuterJoin
+ assert_kind_of Arel::Nodes::OuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a klass" do
+ join = @relation.create_join "foo", "bar", Arel::Nodes::RightOuterJoin
+ assert_kind_of Arel::Nodes::RightOuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should return an insert manager" do
+ im = @relation.compile_insert "VALUES(NULL)"
+ assert_kind_of Arel::InsertManager, im
+ im.into Table.new(:users)
+ assert_equal "INSERT INTO \"users\" VALUES(NULL)", im.to_sql
+ end
+
+ describe "skip" do
+ it "should add an offset" do
+ sm = @relation.skip 2
+ sm.to_sql.must_be_like "SELECT FROM \"users\" OFFSET 2"
+ end
+ end
+
+ describe "having" do
+ it "adds a having clause" do
+ mgr = @relation.having @relation[:id].eq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT FROM "users" HAVING "users"."id" = 10
+ }
+ end
+ end
+
+ describe "backwards compat" do
+ describe "join" do
+ it "noops on nil" do
+ mgr = @relation.join nil
+
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" }
+ end
+
+ it "raises EmptyJoinError on empty" do
+ assert_raises(EmptyJoinError) do
+ @relation.join ""
+ end
+ end
+
+ it "takes a second argument for join type" do
+ right = @relation.alias
+ predicate = @relation[:id].eq(right[:id])
+ mgr = @relation.join(right, Nodes::OuterJoin).on(predicate)
+
+ mgr.to_sql.must_be_like %{
+ SELECT FROM "users"
+ LEFT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+ end
+
+ describe "join" do
+ it "creates an outer join" do
+ right = @relation.alias
+ predicate = @relation[:id].eq(right[:id])
+ mgr = @relation.outer_join(right).on(predicate)
+
+ mgr.to_sql.must_be_like %{
+ SELECT FROM "users"
+ LEFT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+ end
+ end
+
+ describe "group" do
+ it "should create a group" do
+ manager = @relation.group @relation[:id]
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" GROUP BY "users"."id"
+ }
+ end
+ end
+
+ describe "alias" do
+ it "should create a node that proxies to a table" do
+ node = @relation.alias
+ node.name.must_equal "users_2"
+ node[:id].relation.must_equal node
+ end
+ end
+
+ describe "new" do
+ it "should accept a hash" do
+ rel = Table.new :users, as: "foo"
+ rel.table_alias.must_equal "foo"
+ end
+
+ it "ignores as if it equals name" do
+ rel = Table.new :users, as: "users"
+ rel.table_alias.must_be_nil
+ end
+ end
+
+ describe "order" do
+ it "should take an order" do
+ manager = @relation.order "foo"
+ manager.to_sql.must_be_like %{ SELECT FROM "users" ORDER BY foo }
+ end
+ end
+
+ describe "take" do
+ it "should add a limit" do
+ manager = @relation.take 1
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.to_sql.must_be_like %{ SELECT * FROM "users" LIMIT 1 }
+ end
+ end
+
+ describe "project" do
+ it "can project" do
+ manager = @relation.project Nodes::SqlLiteral.new "*"
+ manager.to_sql.must_be_like %{ SELECT * FROM "users" }
+ end
+
+ it "takes multiple parameters" do
+ manager = @relation.project Nodes::SqlLiteral.new("*"), Nodes::SqlLiteral.new("*")
+ manager.to_sql.must_be_like %{ SELECT *, * FROM "users" }
+ end
+ end
+
+ describe "where" do
+ it "returns a tree manager" do
+ manager = @relation.where @relation[:id].eq 1
+ manager.project @relation[:id]
+ manager.must_be_kind_of TreeManager
+ manager.to_sql.must_be_like %{
+ SELECT "users"."id"
+ FROM "users"
+ WHERE "users"."id" = 1
+ }
+ end
+ end
+
+ it "should have a name" do
+ @relation.name.must_equal "users"
+ end
+
+ it "should have a table name" do
+ @relation.table_name.must_equal "users"
+ end
+
+ describe "[]" do
+ describe "when given a Symbol" do
+ it "manufactures an attribute if the symbol names an attribute within the relation" do
+ column = @relation[:id]
+ column.name.must_equal :id
+ end
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ relation1 = Table.new(:users)
+ relation1.table_alias = "zomg"
+ relation2 = Table.new(:users)
+ relation2.table_alias = "zomg"
+ array = [relation1, relation2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ relation1 = Table.new(:users)
+ relation1.table_alias = "zomg"
+ relation2 = Table.new(:users)
+ relation2.table_alias = "zomg2"
+ array = [relation1, relation2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/update_manager_test.rb b/activerecord/test/cases/arel/update_manager_test.rb
new file mode 100644
index 0000000000..cc1b9ac5b3
--- /dev/null
+++ b/activerecord/test/cases/arel/update_manager_test.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class UpdateManagerTest < Arel::Spec
+ describe "new" do
+ it "takes an engine" do
+ Arel::UpdateManager.new
+ end
+ end
+
+ it "should not quote sql literals" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.table table
+ um.set [[table[:name], Arel::Nodes::BindParam.new(1)]]
+ um.to_sql.must_be_like %{ UPDATE "users" SET "name" = ? }
+ end
+
+ it "handles limit properly" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.key = "id"
+ um.take 10
+ um.table table
+ um.set [[table[:name], nil]]
+ assert_match(/LIMIT 10/, um.to_sql)
+ end
+
+ describe "set" do
+ it "updates with null" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.table table
+ um.set [[table[:name], nil]]
+ um.to_sql.must_be_like %{ UPDATE "users" SET "name" = NULL }
+ end
+
+ it "takes a string" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.table table
+ um.set Nodes::SqlLiteral.new "foo = bar"
+ um.to_sql.must_be_like %{ UPDATE "users" SET foo = bar }
+ end
+
+ it "takes a list of lists" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.table table
+ um.set [[table[:id], 1], [table[:name], "hello"]]
+ um.to_sql.must_be_like %{
+ UPDATE "users" SET "id" = 1, "name" = 'hello'
+ }
+ end
+
+ it "chains" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.set([[table[:id], 1], [table[:name], "hello"]]).must_equal um
+ end
+ end
+
+ describe "table" do
+ it "generates an update statement" do
+ um = Arel::UpdateManager.new
+ um.table Table.new(:users)
+ um.to_sql.must_be_like %{ UPDATE "users" }
+ end
+
+ it "chains" do
+ um = Arel::UpdateManager.new
+ um.table(Table.new(:users)).must_equal um
+ end
+
+ it "generates an update statement with joins" do
+ um = Arel::UpdateManager.new
+
+ table = Table.new(:users)
+ join_source = Arel::Nodes::JoinSource.new(
+ table,
+ [table.create_join(Table.new(:posts))]
+ )
+
+ um.table join_source
+ um.to_sql.must_be_like %{ UPDATE "users" INNER JOIN "posts" }
+ end
+ end
+
+ describe "where" do
+ it "generates a where clause" do
+ table = Table.new :users
+ um = Arel::UpdateManager.new
+ um.table table
+ um.where table[:id].eq(1)
+ um.to_sql.must_be_like %{
+ UPDATE "users" WHERE "users"."id" = 1
+ }
+ end
+
+ it "chains" do
+ table = Table.new :users
+ um = Arel::UpdateManager.new
+ um.table table
+ um.where(table[:id].eq(1)).must_equal um
+ end
+ end
+
+ describe "key" do
+ before do
+ @table = Table.new :users
+ @um = Arel::UpdateManager.new
+ @um.key = @table[:foo]
+ end
+
+ it "can be set" do
+ @um.ast.key.must_equal @table[:foo]
+ end
+
+ it "can be accessed" do
+ @um.key.must_equal @table[:foo]
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/depth_first_test.rb b/activerecord/test/cases/arel/visitors/depth_first_test.rb
new file mode 100644
index 0000000000..3baccc7316
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/depth_first_test.rb
@@ -0,0 +1,271 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class TestDepthFirst < Arel::Test
+ Collector = Struct.new(:calls) do
+ def call(object)
+ calls << object
+ end
+ end
+
+ def setup
+ @collector = Collector.new []
+ @visitor = Visitors::DepthFirst.new @collector
+ end
+
+ def test_raises_with_object
+ assert_raises(TypeError) do
+ @visitor.accept(Object.new)
+ end
+ end
+
+
+ # unary ops
+ [
+ Arel::Nodes::Not,
+ Arel::Nodes::Group,
+ Arel::Nodes::On,
+ Arel::Nodes::Grouping,
+ Arel::Nodes::Offset,
+ Arel::Nodes::Ordering,
+ Arel::Nodes::StringJoin,
+ Arel::Nodes::UnqualifiedColumn,
+ Arel::Nodes::Top,
+ Arel::Nodes::Limit,
+ Arel::Nodes::Else,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ op = klass.new(:a)
+ @visitor.accept op
+ assert_equal [:a, op], @collector.calls
+ end
+ end
+
+ # functions
+ [
+ Arel::Nodes::Exists,
+ Arel::Nodes::Avg,
+ Arel::Nodes::Min,
+ Arel::Nodes::Max,
+ Arel::Nodes::Sum,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ func = klass.new(:a, "b")
+ @visitor.accept func
+ assert_equal [:a, "b", false, func], @collector.calls
+ end
+ end
+
+ def test_named_function
+ func = Arel::Nodes::NamedFunction.new(:a, :b, "c")
+ @visitor.accept func
+ assert_equal [:a, :b, false, "c", func], @collector.calls
+ end
+
+ def test_lock
+ lock = Nodes::Lock.new true
+ @visitor.accept lock
+ assert_equal [lock], @collector.calls
+ end
+
+ def test_count
+ count = Nodes::Count.new :a, :b, "c"
+ @visitor.accept count
+ assert_equal [:a, "c", :b, count], @collector.calls
+ end
+
+ def test_inner_join
+ join = Nodes::InnerJoin.new :a, :b
+ @visitor.accept join
+ assert_equal [:a, :b, join], @collector.calls
+ end
+
+ def test_full_outer_join
+ join = Nodes::FullOuterJoin.new :a, :b
+ @visitor.accept join
+ assert_equal [:a, :b, join], @collector.calls
+ end
+
+ def test_outer_join
+ join = Nodes::OuterJoin.new :a, :b
+ @visitor.accept join
+ assert_equal [:a, :b, join], @collector.calls
+ end
+
+ def test_right_outer_join
+ join = Nodes::RightOuterJoin.new :a, :b
+ @visitor.accept join
+ assert_equal [:a, :b, join], @collector.calls
+ end
+
+ [
+ Arel::Nodes::Assignment,
+ Arel::Nodes::Between,
+ Arel::Nodes::Concat,
+ Arel::Nodes::DoesNotMatch,
+ Arel::Nodes::Equality,
+ Arel::Nodes::GreaterThan,
+ Arel::Nodes::GreaterThanOrEqual,
+ Arel::Nodes::In,
+ Arel::Nodes::LessThan,
+ Arel::Nodes::LessThanOrEqual,
+ Arel::Nodes::Matches,
+ Arel::Nodes::NotEqual,
+ Arel::Nodes::NotIn,
+ Arel::Nodes::Or,
+ Arel::Nodes::TableAlias,
+ Arel::Nodes::Values,
+ Arel::Nodes::As,
+ Arel::Nodes::DeleteStatement,
+ Arel::Nodes::JoinSource,
+ Arel::Nodes::When,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ binary = klass.new(:a, :b)
+ @visitor.accept binary
+ assert_equal [:a, :b, binary], @collector.calls
+ end
+ end
+
+ def test_Arel_Nodes_InfixOperation
+ binary = Arel::Nodes::InfixOperation.new(:o, :a, :b)
+ @visitor.accept binary
+ assert_equal [:a, :b, binary], @collector.calls
+ end
+
+ # N-ary
+ [
+ Arel::Nodes::And,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ binary = klass.new([:a, :b, :c])
+ @visitor.accept binary
+ assert_equal [:a, :b, :c, binary], @collector.calls
+ end
+ end
+
+ [
+ Arel::Attributes::Integer,
+ Arel::Attributes::Float,
+ Arel::Attributes::String,
+ Arel::Attributes::Time,
+ Arel::Attributes::Boolean,
+ Arel::Attributes::Attribute
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ binary = klass.new(:a, :b)
+ @visitor.accept binary
+ assert_equal [:a, :b, binary], @collector.calls
+ end
+ end
+
+ def test_table
+ relation = Arel::Table.new(:users)
+ @visitor.accept relation
+ assert_equal ["users", relation], @collector.calls
+ end
+
+ def test_array
+ node = Nodes::Or.new(:a, :b)
+ list = [node]
+ @visitor.accept list
+ assert_equal [:a, :b, node, list], @collector.calls
+ end
+
+ def test_set
+ node = Nodes::Or.new(:a, :b)
+ set = Set.new([node])
+ @visitor.accept set
+ assert_equal [:a, :b, node, set], @collector.calls
+ end
+
+ def test_hash
+ node = Nodes::Or.new(:a, :b)
+ hash = { node => node }
+ @visitor.accept hash
+ assert_equal [:a, :b, node, :a, :b, node, hash], @collector.calls
+ end
+
+ def test_update_statement
+ stmt = Nodes::UpdateStatement.new
+ stmt.relation = :a
+ stmt.values << :b
+ stmt.wheres << :c
+ stmt.orders << :d
+ stmt.limit = :e
+
+ @visitor.accept stmt
+ assert_equal [:a, :b, stmt.values, :c, stmt.wheres, :d, stmt.orders,
+ :e, stmt], @collector.calls
+ end
+
+ def test_select_core
+ core = Nodes::SelectCore.new
+ core.projections << :a
+ core.froms = :b
+ core.wheres << :c
+ core.groups << :d
+ core.windows << :e
+ core.havings << :f
+
+ @visitor.accept core
+ assert_equal [
+ :a, core.projections,
+ :b, [],
+ core.source,
+ :c, core.wheres,
+ :d, core.groups,
+ :e, core.windows,
+ :f, core.havings,
+ core], @collector.calls
+ end
+
+ def test_select_statement
+ ss = Nodes::SelectStatement.new
+ ss.cores.replace [:a]
+ ss.orders << :b
+ ss.limit = :c
+ ss.lock = :d
+ ss.offset = :e
+
+ @visitor.accept ss
+ assert_equal [
+ :a, ss.cores,
+ :b, ss.orders,
+ :c,
+ :d,
+ :e,
+ ss], @collector.calls
+ end
+
+ def test_insert_statement
+ stmt = Nodes::InsertStatement.new
+ stmt.relation = :a
+ stmt.columns << :b
+ stmt.values = :c
+
+ @visitor.accept stmt
+ assert_equal [:a, :b, stmt.columns, :c, stmt], @collector.calls
+ end
+
+ def test_case
+ node = Arel::Nodes::Case.new
+ node.case = :a
+ node.conditions << :b
+ node.default = :c
+
+ @visitor.accept node
+ assert_equal [:a, :b, node.conditions, :c, node], @collector.calls
+ end
+
+ def test_node
+ node = Nodes::Node.new
+ @visitor.accept node
+ assert_equal [node], @collector.calls
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb b/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb
new file mode 100644
index 0000000000..a07a1a050a
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "concurrent"
+
+module Arel
+ module Visitors
+ class DummyVisitor < Visitor
+ def initialize
+ super
+ @barrier = Concurrent::CyclicBarrier.new(2)
+ end
+
+ def visit_Arel_Visitors_DummySuperNode(node)
+ 42
+ end
+
+ # This is terrible, but it's the only way to reliably reproduce
+ # the possible race where two threads attempt to correct the
+ # dispatch hash at the same time.
+ def send(*args)
+ super
+ rescue
+ # Both threads try (and fail) to dispatch to the subclass's name
+ @barrier.wait
+ raise
+ ensure
+ # Then one thread successfully completes (updating the dispatch
+ # table in the process) before the other finishes raising its
+ # exception.
+ Thread.current[:delay].wait if Thread.current[:delay]
+ end
+ end
+
+ class DummySuperNode
+ end
+
+ class DummySubNode < DummySuperNode
+ end
+
+ class DispatchContaminationTest < Arel::Spec
+ before do
+ @connection = Table.engine.connection
+ @table = Table.new(:users)
+ end
+
+ it "dispatches properly after failing upwards" do
+ node = Nodes::Union.new(Nodes::True.new, Nodes::False.new)
+ assert_equal "( TRUE UNION FALSE )", node.to_sql
+
+ node.first # from Nodes::Node's Enumerable mixin
+
+ assert_equal "( TRUE UNION FALSE )", node.to_sql
+ end
+
+ it "is threadsafe when implementing superclass fallback" do
+ visitor = DummyVisitor.new
+ main_thread_finished = Concurrent::Event.new
+
+ racing_thread = Thread.new do
+ Thread.current[:delay] = main_thread_finished
+ visitor.accept DummySubNode.new
+ end
+
+ assert_equal 42, visitor.accept(DummySubNode.new)
+ main_thread_finished.set
+
+ assert_equal 42, racing_thread.value
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/dot_test.rb b/activerecord/test/cases/arel/visitors/dot_test.rb
new file mode 100644
index 0000000000..98f3bab620
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/dot_test.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class TestDot < Arel::Test
+ def setup
+ @visitor = Visitors::Dot.new
+ end
+
+ # functions
+ [
+ Nodes::Sum,
+ Nodes::Exists,
+ Nodes::Max,
+ Nodes::Min,
+ Nodes::Avg,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ op = klass.new(:a, "z")
+ @visitor.accept op, Collectors::PlainString.new
+ end
+ end
+
+ def test_named_function
+ func = Nodes::NamedFunction.new "omg", "omg"
+ @visitor.accept func, Collectors::PlainString.new
+ end
+
+ # unary ops
+ [
+ Arel::Nodes::Not,
+ Arel::Nodes::Group,
+ Arel::Nodes::On,
+ Arel::Nodes::Grouping,
+ Arel::Nodes::Offset,
+ Arel::Nodes::Ordering,
+ Arel::Nodes::UnqualifiedColumn,
+ Arel::Nodes::Top,
+ Arel::Nodes::Limit,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ op = klass.new(:a)
+ @visitor.accept op, Collectors::PlainString.new
+ end
+ end
+
+ # binary ops
+ [
+ Arel::Nodes::Assignment,
+ Arel::Nodes::Between,
+ Arel::Nodes::DoesNotMatch,
+ Arel::Nodes::Equality,
+ Arel::Nodes::GreaterThan,
+ Arel::Nodes::GreaterThanOrEqual,
+ Arel::Nodes::In,
+ Arel::Nodes::LessThan,
+ Arel::Nodes::LessThanOrEqual,
+ Arel::Nodes::Matches,
+ Arel::Nodes::NotEqual,
+ Arel::Nodes::NotIn,
+ Arel::Nodes::Or,
+ Arel::Nodes::TableAlias,
+ Arel::Nodes::Values,
+ Arel::Nodes::As,
+ Arel::Nodes::DeleteStatement,
+ Arel::Nodes::JoinSource,
+ Arel::Nodes::Casted,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ binary = klass.new(:a, :b)
+ @visitor.accept binary, Collectors::PlainString.new
+ end
+ end
+
+ def test_Arel_Nodes_BindParam
+ node = Arel::Nodes::BindParam.new(1)
+ collector = Collectors::PlainString.new
+ assert_match '[label="<f0>Arel::Nodes::BindParam"]', @visitor.accept(node, collector).value
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/ibm_db_test.rb b/activerecord/test/cases/arel/visitors/ibm_db_test.rb
new file mode 100644
index 0000000000..7163cb34d3
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/ibm_db_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class IbmDbTest < Arel::Spec
+ before do
+ @visitor = IBM_DB.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "uses FETCH FIRST n ROWS to limit results" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(1)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT FETCH FIRST 1 ROWS ONLY"
+ end
+
+ it "uses FETCH FIRST n ROWS in updates with a limit" do
+ table = Table.new(:users)
+ stmt = Nodes::UpdateStatement.new
+ stmt.relation = table
+ stmt.limit = Nodes::Limit.new(Nodes.build_quoted(1))
+ stmt.key = table[:id]
+ sql = compile(stmt)
+ sql.must_be_like "UPDATE \"users\" WHERE \"users\".\"id\" IN (SELECT \"users\".\"id\" FROM \"users\" FETCH FIRST 1 ROWS ONLY)"
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/informix_test.rb b/activerecord/test/cases/arel/visitors/informix_test.rb
new file mode 100644
index 0000000000..b0b031cca3
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/informix_test.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class InformixTest < Arel::Spec
+ before do
+ @visitor = Informix.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "uses FIRST n to limit results" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(1)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT FIRST 1"
+ end
+
+ it "uses FIRST n in updates with a limit" do
+ table = Table.new(:users)
+ stmt = Nodes::UpdateStatement.new
+ stmt.relation = table
+ stmt.limit = Nodes::Limit.new(Nodes.build_quoted(1))
+ stmt.key = table[:id]
+ sql = compile(stmt)
+ sql.must_be_like "UPDATE \"users\" WHERE \"users\".\"id\" IN (SELECT FIRST 1 \"users\".\"id\" FROM \"users\")"
+ end
+
+ it "uses SKIP n to jump results" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(10)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT SKIP 10"
+ end
+
+ it "uses SKIP before FIRST" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(1)
+ stmt.offset = Nodes::Offset.new(1)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT SKIP 1 FIRST 1"
+ end
+
+ it "uses INNER JOIN to perform joins" do
+ core = Nodes::SelectCore.new
+ table = Table.new(:posts)
+ core.source = Nodes::JoinSource.new(table, [table.create_join(Table.new(:comments))])
+
+ stmt = Nodes::SelectStatement.new([core])
+ sql = compile(stmt)
+ sql.must_be_like 'SELECT FROM "posts" INNER JOIN "comments"'
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/mssql_test.rb b/activerecord/test/cases/arel/visitors/mssql_test.rb
new file mode 100644
index 0000000000..340376c3d6
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/mssql_test.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class MssqlTest < Arel::Spec
+ before do
+ @visitor = MSSQL.new Table.engine.connection
+ @table = Arel::Table.new "users"
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "should not modify query if no offset or limit" do
+ stmt = Nodes::SelectStatement.new
+ sql = compile(stmt)
+ sql.must_be_like "SELECT"
+ end
+
+ it "should go over table PK if no .order() or .group()" do
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.from = @table
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY \"users\".\"id\") as _row_num FROM \"users\") as _t WHERE _row_num BETWEEN 1 AND 10"
+ end
+
+ it "caches the PK lookup for order" do
+ connection = Minitest::Mock.new
+ connection.expect(:primary_key, ["id"], ["users"])
+
+ # We don't care how many times these methods are called
+ def connection.quote_table_name(*); ""; end
+ def connection.quote_column_name(*); ""; end
+
+ @visitor = MSSQL.new(connection)
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.from = @table
+ stmt.limit = Nodes::Limit.new(10)
+
+ compile(stmt)
+ compile(stmt)
+
+ connection.verify
+ end
+
+ it "should use TOP for limited deletes" do
+ stmt = Nodes::DeleteStatement.new
+ stmt.relation = @table
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile(stmt)
+
+ sql.must_be_like "DELETE TOP (10) FROM \"users\""
+ end
+
+ it "should go over query ORDER BY if .order()" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.orders << Nodes::SqlLiteral.new("order_by")
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY order_by) as _row_num) as _t WHERE _row_num BETWEEN 1 AND 10"
+ end
+
+ it "should go over query GROUP BY if no .order() and there is .group()" do
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.groups << Nodes::SqlLiteral.new("group_by")
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY group_by) as _row_num GROUP BY group_by) as _t WHERE _row_num BETWEEN 1 AND 10"
+ end
+
+ it "should use BETWEEN if both .limit() and .offset" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.offset = Nodes::Offset.new(20)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num BETWEEN 21 AND 30"
+ end
+
+ it "should use >= if only .offset" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(20)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num >= 21"
+ end
+
+ it "should generate subquery for .count" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.cores.first.projections << Nodes::Count.new("*")
+ sql = compile(stmt)
+ sql.must_be_like "SELECT COUNT(1) as count_id FROM (SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num BETWEEN 1 AND 10) AS subquery"
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/mysql_test.rb b/activerecord/test/cases/arel/visitors/mysql_test.rb
new file mode 100644
index 0000000000..9d3bad8516
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/mysql_test.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class MysqlTest < Arel::Spec
+ before do
+ @visitor = MySQL.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "squashes parenthesis on multiple unions" do
+ subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right")
+ node = Nodes::Union.new subnode, Arel.sql("topright")
+ assert_equal 1, compile(node).scan("(").length
+
+ subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right")
+ node = Nodes::Union.new Arel.sql("topleft"), subnode
+ assert_equal 1, compile(node).scan("(").length
+ end
+
+ ###
+ # :'(
+ # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214
+ it "defaults limit to 18446744073709551615" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(1)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT FROM DUAL LIMIT 18446744073709551615 OFFSET 1"
+ end
+
+ it "should escape LIMIT" do
+ sc = Arel::Nodes::UpdateStatement.new
+ sc.relation = Table.new(:users)
+ sc.limit = Nodes::Limit.new(Nodes.build_quoted("omg"))
+ assert_equal("UPDATE \"users\" LIMIT 'omg'", compile(sc))
+ end
+
+ it "uses DUAL for empty from" do
+ stmt = Nodes::SelectStatement.new
+ sql = compile(stmt)
+ sql.must_be_like "SELECT FROM DUAL"
+ end
+
+ describe "locking" do
+ it "defaults to FOR UPDATE when locking" do
+ node = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ compile(node).must_be_like "FOR UPDATE"
+ end
+
+ it "allows a custom string to be used as a lock" do
+ node = Nodes::Lock.new(Arel.sql("LOCK IN SHARE MODE"))
+ compile(node).must_be_like "LOCK IN SHARE MODE"
+ end
+ end
+
+ describe "concat" do
+ it "concats columns" do
+ @table = Table.new(:users)
+ query = @table[:name].concat(@table[:name])
+ compile(query).must_be_like %{
+ CONCAT("users"."name", "users"."name")
+ }
+ end
+
+ it "concats a string" do
+ @table = Table.new(:users)
+ query = @table[:name].concat(Nodes.build_quoted("abc"))
+ compile(query).must_be_like %{
+ CONCAT("users"."name", 'abc')
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/oracle12_test.rb b/activerecord/test/cases/arel/visitors/oracle12_test.rb
new file mode 100644
index 0000000000..83a2ee36ca
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/oracle12_test.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class Oracle12Test < Arel::Spec
+ before do
+ @visitor = Oracle12.new Table.engine.connection
+ @table = Table.new(:users)
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "modified except to be minus" do
+ left = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 10")
+ right = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 20")
+ sql = compile Nodes::Except.new(left, right)
+ sql.must_be_like %{
+ ( SELECT * FROM users WHERE age > 10 MINUS SELECT * FROM users WHERE age > 20 )
+ }
+ end
+
+ it "generates select options offset then limit" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(1)
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT OFFSET 1 ROWS FETCH FIRST 10 ROWS ONLY"
+ end
+
+ describe "locking" do
+ it "generates ArgumentError if limit and lock are used" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.lock = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ assert_raises ArgumentError do
+ compile(stmt)
+ end
+ end
+
+ it "defaults to FOR UPDATE when locking" do
+ node = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ compile(node).must_be_like "FOR UPDATE"
+ end
+ end
+
+ describe "Nodes::BindParam" do
+ it "increments each bind param" do
+ query = @table[:name].eq(Arel::Nodes::BindParam.new(1))
+ .and(@table[:id].eq(Arel::Nodes::BindParam.new(1)))
+ compile(query).must_be_like %{
+ "users"."name" = :a1 AND "users"."id" = :a2
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/oracle_test.rb b/activerecord/test/cases/arel/visitors/oracle_test.rb
new file mode 100644
index 0000000000..e1dfe40cf9
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/oracle_test.rb
@@ -0,0 +1,197 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class OracleTest < Arel::Spec
+ before do
+ @visitor = Oracle.new Table.engine.connection
+ @table = Table.new(:users)
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "modifies order when there is distinct and first value" do
+ # *sigh*
+ select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__"
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new(select)
+ stmt.orders << Nodes::SqlLiteral.new("foo")
+ sql = compile(stmt)
+ sql.must_be_like %{
+ SELECT #{select} ORDER BY alias_0__
+ }
+ end
+
+ it "is idempotent with crazy query" do
+ # *sigh*
+ select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__"
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new(select)
+ stmt.orders << Nodes::SqlLiteral.new("foo")
+
+ sql = compile(stmt)
+ sql2 = compile(stmt)
+ sql.must_equal sql2
+ end
+
+ it "splits orders with commas" do
+ # *sigh*
+ select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__"
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new(select)
+ stmt.orders << Nodes::SqlLiteral.new("foo, bar")
+ sql = compile(stmt)
+ sql.must_be_like %{
+ SELECT #{select} ORDER BY alias_0__, alias_1__
+ }
+ end
+
+ it "splits orders with commas and function calls" do
+ # *sigh*
+ select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__"
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new(select)
+ stmt.orders << Nodes::SqlLiteral.new("NVL(LOWER(bar, foo), foo) DESC, UPPER(baz)")
+ sql = compile(stmt)
+ sql.must_be_like %{
+ SELECT #{select} ORDER BY alias_0__ DESC, alias_1__
+ }
+ end
+
+ describe "Nodes::SelectStatement" do
+ describe "limit" do
+ it "adds a rownum clause" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile stmt
+ sql.must_be_like %{ SELECT WHERE ROWNUM <= 10 }
+ end
+
+ it "is idempotent" do
+ stmt = Nodes::SelectStatement.new
+ stmt.orders << Nodes::SqlLiteral.new("foo")
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile stmt
+ sql2 = compile stmt
+ sql.must_equal sql2
+ end
+
+ it "creates a subquery when there is order_by" do
+ stmt = Nodes::SelectStatement.new
+ stmt.orders << Nodes::SqlLiteral.new("foo")
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (SELECT ORDER BY foo ) WHERE ROWNUM <= 10
+ }
+ end
+
+ it "creates a subquery when there is group by" do
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.groups << Nodes::SqlLiteral.new("foo")
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (SELECT GROUP BY foo ) WHERE ROWNUM <= 10
+ }
+ end
+
+ it "creates a subquery when there is DISTINCT" do
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.set_quantifier = Arel::Nodes::Distinct.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new("id")
+ stmt.limit = Arel::Nodes::Limit.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (SELECT DISTINCT id ) WHERE ROWNUM <= 10
+ }
+ end
+
+ it "creates a different subquery when there is an offset" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.offset = Nodes::Offset.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM (SELECT ) raw_sql_
+ WHERE rownum <= 20
+ )
+ WHERE raw_rnum_ > 10
+ }
+ end
+
+ it "creates a subquery when there is limit and offset with BindParams" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(Nodes::BindParam.new(1))
+ stmt.offset = Nodes::Offset.new(Nodes::BindParam.new(1))
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM (SELECT ) raw_sql_
+ WHERE rownum <= (:a1 + :a2)
+ )
+ WHERE raw_rnum_ > :a3
+ }
+ end
+
+ it "is idempotent with different subquery" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.offset = Nodes::Offset.new(10)
+ sql = compile stmt
+ sql2 = compile stmt
+ sql.must_equal sql2
+ end
+ end
+
+ describe "only offset" do
+ it "creates a select from subquery with rownum condition" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM (SELECT) raw_sql_
+ )
+ WHERE raw_rnum_ > 10
+ }
+ end
+ end
+ end
+
+ it "modified except to be minus" do
+ left = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 10")
+ right = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 20")
+ sql = compile Nodes::Except.new(left, right)
+ sql.must_be_like %{
+ ( SELECT * FROM users WHERE age > 10 MINUS SELECT * FROM users WHERE age > 20 )
+ }
+ end
+
+ describe "locking" do
+ it "defaults to FOR UPDATE when locking" do
+ node = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ compile(node).must_be_like "FOR UPDATE"
+ end
+ end
+
+ describe "Nodes::BindParam" do
+ it "increments each bind param" do
+ query = @table[:name].eq(Arel::Nodes::BindParam.new(1))
+ .and(@table[:id].eq(Arel::Nodes::BindParam.new(1)))
+ compile(query).must_be_like %{
+ "users"."name" = :a1 AND "users"."id" = :a2
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/postgres_test.rb b/activerecord/test/cases/arel/visitors/postgres_test.rb
new file mode 100644
index 0000000000..ba37afecfb
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/postgres_test.rb
@@ -0,0 +1,281 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class PostgresTest < Arel::Spec
+ before do
+ @visitor = PostgreSQL.new Table.engine.connection
+ @table = Table.new(:users)
+ @attr = @table[:id]
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ describe "locking" do
+ it "defaults to FOR UPDATE" do
+ compile(Nodes::Lock.new(Arel.sql("FOR UPDATE"))).must_be_like %{
+ FOR UPDATE
+ }
+ end
+
+ it "allows a custom string to be used as a lock" do
+ node = Nodes::Lock.new(Arel.sql("FOR SHARE"))
+ compile(node).must_be_like %{
+ FOR SHARE
+ }
+ end
+ end
+
+ it "should escape LIMIT" do
+ sc = Arel::Nodes::SelectStatement.new
+ sc.limit = Nodes::Limit.new(Nodes.build_quoted("omg"))
+ sc.cores.first.projections << Arel.sql("DISTINCT ON")
+ sc.orders << Arel.sql("xyz")
+ sql = compile(sc)
+ assert_match(/LIMIT 'omg'/, sql)
+ assert_equal 1, sql.scan(/LIMIT/).length, "should have one limit"
+ end
+
+ it "should support DISTINCT ON" do
+ core = Arel::Nodes::SelectCore.new
+ core.set_quantifier = Arel::Nodes::DistinctOn.new(Arel.sql("aaron"))
+ assert_match "DISTINCT ON ( aaron )", compile(core)
+ end
+
+ it "should support DISTINCT" do
+ core = Arel::Nodes::SelectCore.new
+ core.set_quantifier = Arel::Nodes::Distinct.new
+ assert_equal "SELECT DISTINCT", compile(core)
+ end
+
+ it "encloses LATERAL queries in parens" do
+ subquery = @table.project(:id).where(@table[:name].matches("foo%"))
+ compile(subquery.lateral).must_be_like %{
+ LATERAL (SELECT id FROM "users" WHERE "users"."name" ILIKE 'foo%')
+ }
+ end
+
+ it "produces LATERAL queries with alias" do
+ subquery = @table.project(:id).where(@table[:name].matches("foo%"))
+ compile(subquery.lateral("bar")).must_be_like %{
+ LATERAL (SELECT id FROM "users" WHERE "users"."name" ILIKE 'foo%') bar
+ }
+ end
+
+ describe "Nodes::Matches" do
+ it "should know how to visit" do
+ node = @table[:name].matches("foo%")
+ node.must_be_kind_of Nodes::Matches
+ node.case_sensitive.must_equal(false)
+ compile(node).must_be_like %{
+ "users"."name" ILIKE 'foo%'
+ }
+ end
+
+ it "should know how to visit case sensitive" do
+ node = @table[:name].matches("foo%", nil, true)
+ node.case_sensitive.must_equal(true)
+ compile(node).must_be_like %{
+ "users"."name" LIKE 'foo%'
+ }
+ end
+
+ it "can handle ESCAPE" do
+ node = @table[:name].matches("foo!%", "!")
+ compile(node).must_be_like %{
+ "users"."name" ILIKE 'foo!%' ESCAPE '!'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].matches("foo%"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" ILIKE 'foo%')
+ }
+ end
+ end
+
+ describe "Nodes::DoesNotMatch" do
+ it "should know how to visit" do
+ node = @table[:name].does_not_match("foo%")
+ node.must_be_kind_of Nodes::DoesNotMatch
+ node.case_sensitive.must_equal(false)
+ compile(node).must_be_like %{
+ "users"."name" NOT ILIKE 'foo%'
+ }
+ end
+
+ it "should know how to visit case sensitive" do
+ node = @table[:name].does_not_match("foo%", nil, true)
+ node.case_sensitive.must_equal(true)
+ compile(node).must_be_like %{
+ "users"."name" NOT LIKE 'foo%'
+ }
+ end
+
+ it "can handle ESCAPE" do
+ node = @table[:name].does_not_match("foo!%", "!")
+ compile(node).must_be_like %{
+ "users"."name" NOT ILIKE 'foo!%' ESCAPE '!'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].does_not_match("foo%"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" NOT ILIKE 'foo%')
+ }
+ end
+ end
+
+ describe "Nodes::Regexp" do
+ it "should know how to visit" do
+ node = @table[:name].matches_regexp("foo.*")
+ node.must_be_kind_of Nodes::Regexp
+ node.case_sensitive.must_equal(true)
+ compile(node).must_be_like %{
+ "users"."name" ~ 'foo.*'
+ }
+ end
+
+ it "can handle case insensitive" do
+ node = @table[:name].matches_regexp("foo.*", false)
+ node.must_be_kind_of Nodes::Regexp
+ node.case_sensitive.must_equal(false)
+ compile(node).must_be_like %{
+ "users"."name" ~* 'foo.*'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].matches_regexp("foo.*"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" ~ 'foo.*')
+ }
+ end
+ end
+
+ describe "Nodes::NotRegexp" do
+ it "should know how to visit" do
+ node = @table[:name].does_not_match_regexp("foo.*")
+ node.must_be_kind_of Nodes::NotRegexp
+ node.case_sensitive.must_equal(true)
+ compile(node).must_be_like %{
+ "users"."name" !~ 'foo.*'
+ }
+ end
+
+ it "can handle case insensitive" do
+ node = @table[:name].does_not_match_regexp("foo.*", false)
+ node.case_sensitive.must_equal(false)
+ compile(node).must_be_like %{
+ "users"."name" !~* 'foo.*'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].does_not_match_regexp("foo.*"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" !~ 'foo.*')
+ }
+ end
+ end
+
+ describe "Nodes::BindParam" do
+ it "increments each bind param" do
+ query = @table[:name].eq(Arel::Nodes::BindParam.new(1))
+ .and(@table[:id].eq(Arel::Nodes::BindParam.new(1)))
+ compile(query).must_be_like %{
+ "users"."name" = $1 AND "users"."id" = $2
+ }
+ end
+ end
+
+ describe "Nodes::Cube" do
+ it "should know how to visit with array arguments" do
+ node = Arel::Nodes::Cube.new([@table[:name], @table[:bool]])
+ compile(node).must_be_like %{
+ CUBE( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to visit with CubeDimension Argument" do
+ dimensions = Arel::Nodes::GroupingElement.new([@table[:name], @table[:bool]])
+ node = Arel::Nodes::Cube.new(dimensions)
+ compile(node).must_be_like %{
+ CUBE( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to generate paranthesis when supplied with many Dimensions" do
+ dim1 = Arel::Nodes::GroupingElement.new(@table[:name])
+ dim2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]])
+ node = Arel::Nodes::Cube.new([dim1, dim2])
+ compile(node).must_be_like %{
+ CUBE( ( "users"."name" ), ( "users"."bool", "users"."created_at" ) )
+ }
+ end
+ end
+
+ describe "Nodes::GroupingSet" do
+ it "should know how to visit with array arguments" do
+ node = Arel::Nodes::GroupingSet.new([@table[:name], @table[:bool]])
+ compile(node).must_be_like %{
+ GROUPING SET( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to visit with CubeDimension Argument" do
+ group = Arel::Nodes::GroupingElement.new([@table[:name], @table[:bool]])
+ node = Arel::Nodes::GroupingSet.new(group)
+ compile(node).must_be_like %{
+ GROUPING SET( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to generate paranthesis when supplied with many Dimensions" do
+ group1 = Arel::Nodes::GroupingElement.new(@table[:name])
+ group2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]])
+ node = Arel::Nodes::GroupingSet.new([group1, group2])
+ compile(node).must_be_like %{
+ GROUPING SET( ( "users"."name" ), ( "users"."bool", "users"."created_at" ) )
+ }
+ end
+ end
+
+ describe "Nodes::RollUp" do
+ it "should know how to visit with array arguments" do
+ node = Arel::Nodes::RollUp.new([@table[:name], @table[:bool]])
+ compile(node).must_be_like %{
+ ROLLUP( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to visit with CubeDimension Argument" do
+ group = Arel::Nodes::GroupingElement.new([@table[:name], @table[:bool]])
+ node = Arel::Nodes::RollUp.new(group)
+ compile(node).must_be_like %{
+ ROLLUP( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to generate paranthesis when supplied with many Dimensions" do
+ group1 = Arel::Nodes::GroupingElement.new(@table[:name])
+ group2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]])
+ node = Arel::Nodes::RollUp.new([group1, group2])
+ compile(node).must_be_like %{
+ ROLLUP( ( "users"."name" ), ( "users"."bool", "users"."created_at" ) )
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/sqlite_test.rb b/activerecord/test/cases/arel/visitors/sqlite_test.rb
new file mode 100644
index 0000000000..6650b6ff3a
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/sqlite_test.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class SqliteTest < Arel::Spec
+ before do
+ @visitor = SQLite.new Table.engine.connection_pool
+ end
+
+ it "defaults limit to -1" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(1)
+ sql = @visitor.accept(stmt, Collectors::SQLString.new).value
+ sql.must_be_like "SELECT LIMIT -1 OFFSET 1"
+ end
+
+ it "does not support locking" do
+ node = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ assert_equal "", @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "does not support boolean" do
+ node = Nodes::True.new()
+ assert_equal "1", @visitor.accept(node, Collectors::SQLString.new).value
+ node = Nodes::False.new()
+ assert_equal "0", @visitor.accept(node, Collectors::SQLString.new).value
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/to_sql_test.rb b/activerecord/test/cases/arel/visitors/to_sql_test.rb
new file mode 100644
index 0000000000..ce836eded7
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb
@@ -0,0 +1,654 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "bigdecimal"
+
+module Arel
+ module Visitors
+ describe "the to_sql visitor" do
+ before do
+ @conn = FakeRecord::Base.new
+ @visitor = ToSql.new @conn.connection
+ @table = Table.new(:users)
+ @attr = @table[:id]
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "works with BindParams" do
+ node = Nodes::BindParam.new(1)
+ sql = compile node
+ sql.must_be_like "?"
+ end
+
+ it "does not quote BindParams used as part of a Values" do
+ bp = Nodes::BindParam.new(1)
+ values = Nodes::Values.new([bp])
+ sql = compile values
+ sql.must_be_like "VALUES (?)"
+ end
+
+ it "can define a dispatch method" do
+ visited = false
+ viz = Class.new(Arel::Visitors::Visitor) {
+ define_method(:hello) do |node, c|
+ visited = true
+ end
+
+ def dispatch
+ { Arel::Table => "hello" }
+ end
+ }.new
+
+ viz.accept(@table, Collectors::SQLString.new)
+ assert visited, "hello method was called"
+ end
+
+ it "should not quote sql literals" do
+ node = @table[Arel.star]
+ sql = compile node
+ sql.must_be_like '"users".*'
+ end
+
+ it "should visit named functions" do
+ function = Nodes::NamedFunction.new("omg", [Arel.star])
+ assert_equal "omg(*)", compile(function)
+ end
+
+ it "should chain predications on named functions" do
+ function = Nodes::NamedFunction.new("omg", [Arel.star])
+ sql = compile(function.eq(2))
+ sql.must_be_like %{ omg(*) = 2 }
+ end
+
+ it "should handle nil with named functions" do
+ function = Nodes::NamedFunction.new("omg", [Arel.star])
+ sql = compile(function.eq(nil))
+ sql.must_be_like %{ omg(*) IS NULL }
+ end
+
+ it "should visit built-in functions" do
+ function = Nodes::Count.new([Arel.star])
+ assert_equal "COUNT(*)", compile(function)
+
+ function = Nodes::Sum.new([Arel.star])
+ assert_equal "SUM(*)", compile(function)
+
+ function = Nodes::Max.new([Arel.star])
+ assert_equal "MAX(*)", compile(function)
+
+ function = Nodes::Min.new([Arel.star])
+ assert_equal "MIN(*)", compile(function)
+
+ function = Nodes::Avg.new([Arel.star])
+ assert_equal "AVG(*)", compile(function)
+ end
+
+ it "should visit built-in functions operating on distinct values" do
+ function = Nodes::Count.new([Arel.star])
+ function.distinct = true
+ assert_equal "COUNT(DISTINCT *)", compile(function)
+
+ function = Nodes::Sum.new([Arel.star])
+ function.distinct = true
+ assert_equal "SUM(DISTINCT *)", compile(function)
+
+ function = Nodes::Max.new([Arel.star])
+ function.distinct = true
+ assert_equal "MAX(DISTINCT *)", compile(function)
+
+ function = Nodes::Min.new([Arel.star])
+ function.distinct = true
+ assert_equal "MIN(DISTINCT *)", compile(function)
+
+ function = Nodes::Avg.new([Arel.star])
+ function.distinct = true
+ assert_equal "AVG(DISTINCT *)", compile(function)
+ end
+
+ it "works with lists" do
+ function = Nodes::NamedFunction.new("omg", [Arel.star, Arel.star])
+ assert_equal "omg(*, *)", compile(function)
+ end
+
+ describe "Nodes::Equality" do
+ it "should escape strings" do
+ test = Table.new(:users)[:name].eq "Aaron Patterson"
+ compile(test).must_be_like %{
+ "users"."name" = 'Aaron Patterson'
+ }
+ end
+
+ it "should handle false" do
+ table = Table.new(:users)
+ val = Nodes.build_quoted(false, table[:active])
+ sql = compile Nodes::Equality.new(val, val)
+ sql.must_be_like %{ 'f' = 'f' }
+ end
+
+ it "should handle nil" do
+ sql = compile Nodes::Equality.new(@table[:name], nil)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::Grouping" do
+ it "wraps nested groupings in brackets only once" do
+ sql = compile Nodes::Grouping.new(Nodes::Grouping.new(Nodes.build_quoted("foo")))
+ sql.must_equal "('foo')"
+ end
+ end
+
+ describe "Nodes::NotEqual" do
+ it "should handle false" do
+ val = Nodes.build_quoted(false, @table[:active])
+ sql = compile Nodes::NotEqual.new(@table[:active], val)
+ sql.must_be_like %{ "users"."active" != 'f' }
+ end
+
+ it "should handle nil" do
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::NotEqual.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
+
+ it "should visit string subclass" do
+ [
+ Class.new(String).new(":'("),
+ Class.new(Class.new(String)).new(":'("),
+ ].each do |obj|
+ val = Nodes.build_quoted(obj, @table[:active])
+ sql = compile Nodes::NotEqual.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" != ':\\'(' }
+ end
+ end
+
+ it "should visit_Class" do
+ compile(Nodes.build_quoted(DateTime)).must_equal "'DateTime'"
+ end
+
+ it "should escape LIMIT" do
+ sc = Arel::Nodes::SelectStatement.new
+ sc.limit = Arel::Nodes::Limit.new(Nodes.build_quoted("omg"))
+ assert_match(/LIMIT 'omg'/, compile(sc))
+ end
+
+ it "should contain a single space before ORDER BY" do
+ table = Table.new(:users)
+ test = table.order(table[:name])
+ sql = compile test
+ assert_match(/"users" ORDER BY/, sql)
+ end
+
+ it "should quote LIMIT without column type coercion" do
+ table = Table.new(:users)
+ sc = table.where(table[:name].eq(0)).take(1).ast
+ assert_match(/WHERE "users"."name" = 0 LIMIT 1/, compile(sc))
+ end
+
+ it "should visit_DateTime" do
+ dt = DateTime.now
+ table = Table.new(:users)
+ test = table[:created_at].eq dt
+ sql = compile test
+
+ sql.must_be_like %{"users"."created_at" = '#{dt.strftime("%Y-%m-%d %H:%M:%S")}'}
+ end
+
+ it "should visit_Float" do
+ test = Table.new(:products)[:price].eq 2.14
+ sql = compile test
+ sql.must_be_like %{"products"."price" = 2.14}
+ end
+
+ it "should visit_Not" do
+ sql = compile Nodes::Not.new(Arel.sql("foo"))
+ sql.must_be_like "NOT (foo)"
+ end
+
+ it "should apply Not to the whole expression" do
+ node = Nodes::And.new [@attr.eq(10), @attr.eq(11)]
+ sql = compile Nodes::Not.new(node)
+ sql.must_be_like %{NOT ("users"."id" = 10 AND "users"."id" = 11)}
+ end
+
+ it "should visit_As" do
+ as = Nodes::As.new(Arel.sql("foo"), Arel.sql("bar"))
+ sql = compile as
+ sql.must_be_like "foo AS bar"
+ end
+
+ it "should visit_Bignum" do
+ compile 8787878092
+ end
+
+ it "should visit_Hash" do
+ compile(Nodes.build_quoted(a: 1))
+ end
+
+ it "should visit_Set" do
+ compile Nodes.build_quoted(Set.new([1, 2]))
+ end
+
+ it "should visit_BigDecimal" do
+ compile Nodes.build_quoted(BigDecimal("2.14"))
+ end
+
+ it "should visit_Date" do
+ dt = Date.today
+ table = Table.new(:users)
+ test = table[:created_at].eq dt
+ sql = compile test
+
+ sql.must_be_like %{"users"."created_at" = '#{dt.strftime("%Y-%m-%d")}'}
+ end
+
+ it "should visit_NilClass" do
+ compile(Nodes.build_quoted(nil)).must_be_like "NULL"
+ end
+
+ it "unsupported input should raise UnsupportedVisitError" do
+ error = assert_raises(UnsupportedVisitError) { compile(nil) }
+ assert_match(/\AUnsupported/, error.message)
+ end
+
+ it "should visit_Arel_SelectManager, which is a subquery" do
+ mgr = Table.new(:foo).project(:bar)
+ compile(mgr).must_be_like '(SELECT bar FROM "foo")'
+ end
+
+ it "should visit_Arel_Nodes_And" do
+ node = Nodes::And.new [@attr.eq(10), @attr.eq(11)]
+ compile(node).must_be_like %{
+ "users"."id" = 10 AND "users"."id" = 11
+ }
+ end
+
+ it "should visit_Arel_Nodes_Or" do
+ node = Nodes::Or.new @attr.eq(10), @attr.eq(11)
+ compile(node).must_be_like %{
+ "users"."id" = 10 OR "users"."id" = 11
+ }
+ end
+
+ it "should visit_Arel_Nodes_Assignment" do
+ column = @table["id"]
+ node = Nodes::Assignment.new(
+ Nodes::UnqualifiedColumn.new(column),
+ Nodes::UnqualifiedColumn.new(column)
+ )
+ compile(node).must_be_like %{
+ "id" = "id"
+ }
+ end
+
+ it "should visit visit_Arel_Attributes_Time" do
+ attr = Attributes::Time.new(@attr.relation, @attr.name)
+ compile attr
+ end
+
+ it "should visit_TrueClass" do
+ test = Table.new(:users)[:bool].eq(true)
+ compile(test).must_be_like %{ "users"."bool" = 't' }
+ end
+
+ describe "Nodes::Matches" do
+ it "should know how to visit" do
+ node = @table[:name].matches("foo%")
+ compile(node).must_be_like %{
+ "users"."name" LIKE 'foo%'
+ }
+ end
+
+ it "can handle ESCAPE" do
+ node = @table[:name].matches("foo!%", "!")
+ compile(node).must_be_like %{
+ "users"."name" LIKE 'foo!%' ESCAPE '!'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].matches("foo%"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" LIKE 'foo%')
+ }
+ end
+ end
+
+ describe "Nodes::DoesNotMatch" do
+ it "should know how to visit" do
+ node = @table[:name].does_not_match("foo%")
+ compile(node).must_be_like %{
+ "users"."name" NOT LIKE 'foo%'
+ }
+ end
+
+ it "can handle ESCAPE" do
+ node = @table[:name].does_not_match("foo!%", "!")
+ compile(node).must_be_like %{
+ "users"."name" NOT LIKE 'foo!%' ESCAPE '!'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].does_not_match("foo%"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" NOT LIKE 'foo%')
+ }
+ end
+ end
+
+ describe "Nodes::Ordering" do
+ it "should know how to visit" do
+ node = @attr.desc
+ compile(node).must_be_like %{
+ "users"."id" DESC
+ }
+ end
+ end
+
+ describe "Nodes::In" do
+ it "should know how to visit" do
+ node = @attr.in [1, 2, 3]
+ compile(node).must_be_like %{
+ "users"."id" IN (1, 2, 3)
+ }
+ end
+
+ it "should return 1=0 when empty right which is always false" do
+ node = @attr.in []
+ compile(node).must_equal "1=0"
+ end
+
+ it "can handle two dot ranges" do
+ node = @attr.between 1..3
+ compile(node).must_be_like %{
+ "users"."id" BETWEEN 1 AND 3
+ }
+ end
+
+ it "can handle three dot ranges" do
+ node = @attr.between 1...3
+ compile(node).must_be_like %{
+ "users"."id" >= 1 AND "users"."id" < 3
+ }
+ end
+
+ it "can handle ranges bounded by infinity" do
+ node = @attr.between 1..Float::INFINITY
+ compile(node).must_be_like %{
+ "users"."id" >= 1
+ }
+ node = @attr.between(-Float::INFINITY..3)
+ compile(node).must_be_like %{
+ "users"."id" <= 3
+ }
+ node = @attr.between(-Float::INFINITY...3)
+ compile(node).must_be_like %{
+ "users"."id" < 3
+ }
+ node = @attr.between(-Float::INFINITY..Float::INFINITY)
+ compile(node).must_be_like %{1=1}
+ end
+
+ it "can handle subqueries" do
+ table = Table.new(:users)
+ subquery = table.project(:id).where(table[:name].eq("Aaron"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" = 'Aaron')
+ }
+ end
+ end
+
+ describe "Nodes::InfixOperation" do
+ it "should handle Multiplication" do
+ node = Arel::Attributes::Decimal.new(Table.new(:products), :price) * Arel::Attributes::Decimal.new(Table.new(:currency_rates), :rate)
+ compile(node).must_equal %("products"."price" * "currency_rates"."rate")
+ end
+
+ it "should handle Division" do
+ node = Arel::Attributes::Decimal.new(Table.new(:products), :price) / 5
+ compile(node).must_equal %("products"."price" / 5)
+ end
+
+ it "should handle Addition" do
+ node = Arel::Attributes::Decimal.new(Table.new(:products), :price) + 6
+ compile(node).must_equal %(("products"."price" + 6))
+ end
+
+ it "should handle Subtraction" do
+ node = Arel::Attributes::Decimal.new(Table.new(:products), :price) - 7
+ compile(node).must_equal %(("products"."price" - 7))
+ end
+
+ it "should handle Concatination" do
+ table = Table.new(:users)
+ node = table[:name].concat(table[:name])
+ compile(node).must_equal %("users"."name" || "users"."name")
+ end
+
+ it "should handle BitwiseAnd" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) & 16
+ compile(node).must_equal %(("products"."bitmap" & 16))
+ end
+
+ it "should handle BitwiseOr" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) | 16
+ compile(node).must_equal %(("products"."bitmap" | 16))
+ end
+
+ it "should handle BitwiseXor" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) ^ 16
+ compile(node).must_equal %(("products"."bitmap" ^ 16))
+ end
+
+ it "should handle BitwiseShiftLeft" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) << 4
+ compile(node).must_equal %(("products"."bitmap" << 4))
+ end
+
+ it "should handle BitwiseShiftRight" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) >> 4
+ compile(node).must_equal %(("products"."bitmap" >> 4))
+ end
+
+ it "should handle arbitrary operators" do
+ node = Arel::Nodes::InfixOperation.new(
+ "&&",
+ Arel::Attributes::String.new(Table.new(:products), :name),
+ Arel::Attributes::String.new(Table.new(:products), :name)
+ )
+ compile(node).must_equal %("products"."name" && "products"."name")
+ end
+ end
+
+ describe "Nodes::UnaryOperation" do
+ it "should handle BitwiseNot" do
+ node = ~ Arel::Attributes::Integer.new(Table.new(:products), :bitmap)
+ compile(node).must_equal %( ~ "products"."bitmap")
+ end
+
+ it "should handle arbitrary operators" do
+ node = Arel::Nodes::UnaryOperation.new("!", Arel::Attributes::String.new(Table.new(:products), :active))
+ compile(node).must_equal %( ! "products"."active")
+ end
+ end
+
+ describe "Nodes::NotIn" do
+ it "should know how to visit" do
+ node = @attr.not_in [1, 2, 3]
+ compile(node).must_be_like %{
+ "users"."id" NOT IN (1, 2, 3)
+ }
+ end
+
+ it "should return 1=1 when empty right which is always true" do
+ node = @attr.not_in []
+ compile(node).must_equal "1=1"
+ end
+
+ it "can handle two dot ranges" do
+ node = @attr.not_between 1..3
+ compile(node).must_equal(
+ %{("users"."id" < 1 OR "users"."id" > 3)}
+ )
+ end
+
+ it "can handle three dot ranges" do
+ node = @attr.not_between 1...3
+ compile(node).must_equal(
+ %{("users"."id" < 1 OR "users"."id" >= 3)}
+ )
+ end
+
+ it "can handle ranges bounded by infinity" do
+ node = @attr.not_between 1..Float::INFINITY
+ compile(node).must_be_like %{
+ "users"."id" < 1
+ }
+ node = @attr.not_between(-Float::INFINITY..3)
+ compile(node).must_be_like %{
+ "users"."id" > 3
+ }
+ node = @attr.not_between(-Float::INFINITY...3)
+ compile(node).must_be_like %{
+ "users"."id" >= 3
+ }
+ node = @attr.not_between(-Float::INFINITY..Float::INFINITY)
+ compile(node).must_be_like %{1=0}
+ end
+
+ it "can handle subqueries" do
+ table = Table.new(:users)
+ subquery = table.project(:id).where(table[:name].eq("Aaron"))
+ node = @attr.not_in subquery
+ compile(node).must_be_like %{
+ "users"."id" NOT IN (SELECT id FROM "users" WHERE "users"."name" = 'Aaron')
+ }
+ end
+ end
+
+ describe "Constants" do
+ it "should handle true" do
+ test = Table.new(:users).create_true
+ compile(test).must_be_like %{
+ TRUE
+ }
+ end
+
+ it "should handle false" do
+ test = Table.new(:users).create_false
+ compile(test).must_be_like %{
+ FALSE
+ }
+ end
+ end
+
+ describe "TableAlias" do
+ it "should use the underlying table for checking columns" do
+ test = Table.new(:users).alias("zomgusers")[:id].eq "3"
+ compile(test).must_be_like %{
+ "zomgusers"."id" = '3'
+ }
+ end
+ end
+
+ describe "distinct on" do
+ it "raises not implemented error" do
+ core = Arel::Nodes::SelectCore.new
+ core.set_quantifier = Arel::Nodes::DistinctOn.new(Arel.sql("aaron"))
+
+ assert_raises(NotImplementedError) do
+ compile(core)
+ end
+ end
+ end
+
+ describe "Nodes::Regexp" do
+ it "raises not implemented error" do
+ node = Arel::Nodes::Regexp.new(@table[:name], Nodes.build_quoted("foo%"))
+
+ assert_raises(NotImplementedError) do
+ compile(node)
+ end
+ end
+ end
+
+ describe "Nodes::NotRegexp" do
+ it "raises not implemented error" do
+ node = Arel::Nodes::NotRegexp.new(@table[:name], Nodes.build_quoted("foo%"))
+
+ assert_raises(NotImplementedError) do
+ compile(node)
+ end
+ end
+ end
+
+ describe "Nodes::Case" do
+ it "supports simple case expressions" do
+ node = Arel::Nodes::Case.new(@table[:name])
+ .when("foo").then(1)
+ .else(0)
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 1 ELSE 0 END
+ }
+ end
+
+ it "supports extended case expressions" do
+ node = Arel::Nodes::Case.new
+ .when(@table[:name].in(%w(foo bar))).then(1)
+ .else(0)
+
+ compile(node).must_be_like %{
+ CASE WHEN "users"."name" IN ('foo', 'bar') THEN 1 ELSE 0 END
+ }
+ end
+
+ it "works without default branch" do
+ node = Arel::Nodes::Case.new(@table[:name])
+ .when("foo").then(1)
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 1 END
+ }
+ end
+
+ it "allows chaining multiple conditions" do
+ node = Arel::Nodes::Case.new(@table[:name])
+ .when("foo").then(1)
+ .when("bar").then(2)
+ .else(0)
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 1 WHEN 'bar' THEN 2 ELSE 0 END
+ }
+ end
+
+ it "supports #when with two arguments and no #then" do
+ node = Arel::Nodes::Case.new @table[:name]
+
+ { foo: 1, bar: 0 }.reduce(node) { |_node, pair| _node.when(*pair) }
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 1 WHEN 'bar' THEN 0 END
+ }
+ end
+
+ it "can be chained as a predicate" do
+ node = @table[:name].when("foo").then("bar").else("baz")
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 'bar' ELSE 'baz' END
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 0f7a249bf3..a85b56ac4b 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -37,6 +37,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal companies(:first_firm).name, firm.name
end
+ def test_assigning_belongs_to_on_destroyed_object
+ client = Client.create!(name: "Client")
+ client.destroy!
+ assert_raise(frozen_error_class) { client.firm = nil }
+ assert_raise(frozen_error_class) { client.firm = Firm.new(name: "Firm") }
+ end
+
def test_missing_attribute_error_is_raised_when_no_foreign_key_attribute
assert_raises(ActiveModel::MissingAttributeError) { Client.select(:id).first.firm }
end
@@ -79,7 +86,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
account = model.new
- assert account.valid?
+ assert_predicate account, :valid?
ensure
ActiveRecord::Base.belongs_to_required_by_default = original_value
end
@@ -95,7 +102,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
account = model.new
- assert_not account.valid?
+ assert_not_predicate account, :valid?
assert_equal [{ error: :blank }], account.errors.details[:company]
ensure
ActiveRecord::Base.belongs_to_required_by_default = original_value
@@ -112,7 +119,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
account = model.new
- assert_not account.valid?
+ assert_not_predicate account, :valid?
assert_equal [{ error: :blank }], account.errors.details[:company]
ensure
ActiveRecord::Base.belongs_to_required_by_default = original_value
@@ -246,14 +253,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
Firm.create("name" => "Apple")
Client.create("name" => "Citibank", :firm_name => "Apple")
citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key).first
- assert citibank_result.association(:firm_with_primary_key).loaded?
+ assert_predicate citibank_result.association(:firm_with_primary_key), :loaded?
end
def test_eager_loading_with_primary_key_as_symbol
Firm.create("name" => "Apple")
Client.create("name" => "Citibank", :firm_name => "Apple")
citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key_symbols).first
- assert citibank_result.association(:firm_with_primary_key_symbols).loaded?
+ assert_predicate citibank_result.association(:firm_with_primary_key_symbols), :loaded?
end
def test_creating_the_belonging_object
@@ -320,7 +327,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
client = Client.create!(name: "Jimmy")
account = client.create_account!(credit_limit: 10)
assert_equal account, client.account
- assert account.persisted?
+ assert_predicate account, :persisted?
client.save
client.reload
assert_equal account, client.account
@@ -330,7 +337,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
client = Client.create!(name: "Jimmy")
assert_raise(ActiveRecord::RecordInvalid) { client.create_account! }
assert_not_nil client.account
- assert client.account.new_record?
+ assert_predicate client.account, :new_record?
end
def test_reloading_the_belonging_object
@@ -627,10 +634,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
final_cut.firm = firm
- assert !final_cut.persisted?
+ assert_not_predicate final_cut, :persisted?
assert final_cut.save
- assert final_cut.persisted?
- assert firm.persisted?
+ assert_predicate final_cut, :persisted?
+ assert_predicate firm, :persisted?
assert_equal firm, final_cut.firm
final_cut.association(:firm).reload
assert_equal firm, final_cut.firm
@@ -640,10 +647,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
final_cut.firm_with_primary_key = firm
- assert !final_cut.persisted?
+ assert_not_predicate final_cut, :persisted?
assert final_cut.save
- assert final_cut.persisted?
- assert firm.persisted?
+ assert_predicate final_cut, :persisted?
+ assert_predicate firm, :persisted?
assert_equal firm, final_cut.firm_with_primary_key
final_cut.association(:firm_with_primary_key).reload
assert_equal firm, final_cut.firm_with_primary_key
@@ -790,7 +797,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_cant_save_readonly_association
assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_client).readonly_firm.save! }
- assert companies(:first_client).readonly_firm.readonly?
+ assert_predicate companies(:first_client).readonly_firm, :readonly?
end
def test_polymorphic_assignment_foreign_key_type_string
@@ -931,6 +938,30 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal error.message, "The :dependent option must be one of [:destroy, :delete], but is :nullify"
end
+ class DestroyableBook < ActiveRecord::Base
+ self.table_name = "books"
+ belongs_to :author, class_name: "UndestroyableAuthor", dependent: :destroy
+ end
+
+ class UndestroyableAuthor < ActiveRecord::Base
+ self.table_name = "authors"
+ has_one :book, class_name: "DestroyableBook", foreign_key: "author_id"
+ before_destroy :dont
+
+ def dont
+ throw(:abort)
+ end
+ end
+
+ def test_dependency_should_halt_parent_destruction
+ author = UndestroyableAuthor.create!(name: "Test")
+ book = DestroyableBook.create!(author: author)
+
+ assert_no_difference ["UndestroyableAuthor.count", "DestroyableBook.count"] do
+ assert_not book.destroy
+ end
+ end
+
def test_attributes_are_being_set_when_initialized_from_belongs_to_association_with_where_clause
new_firm = accounts(:signals37).build_firm(name: "Apple")
assert_equal new_firm.name, "Apple"
@@ -949,15 +980,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
firm_proxy = client.send(:association_instance_get, :firm)
firm_with_condition_proxy = client.send(:association_instance_get, :firm_with_condition)
- assert !firm_proxy.stale_target?
- assert !firm_with_condition_proxy.stale_target?
+ assert_not_predicate firm_proxy, :stale_target?
+ assert_not_predicate firm_with_condition_proxy, :stale_target?
assert_equal companies(:first_firm), client.firm
assert_equal companies(:first_firm), client.firm_with_condition
client.client_of = companies(:another_firm).id
- assert firm_proxy.stale_target?
- assert firm_with_condition_proxy.stale_target?
+ assert_predicate firm_proxy, :stale_target?
+ assert_predicate firm_with_condition_proxy, :stale_target?
assert_equal companies(:another_firm), client.firm
assert_equal companies(:another_firm), client.firm_with_condition
end
@@ -968,12 +999,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
sponsor.sponsorable
proxy = sponsor.send(:association_instance_get, :sponsorable)
- assert !proxy.stale_target?
+ assert_not_predicate proxy, :stale_target?
assert_equal members(:groucho), sponsor.sponsorable
sponsor.sponsorable_id = members(:some_other_guy).id
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal members(:some_other_guy), sponsor.sponsorable
end
@@ -983,12 +1014,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
sponsor.sponsorable
proxy = sponsor.send(:association_instance_get, :sponsorable)
- assert !proxy.stale_target?
+ assert_not_predicate proxy, :stale_target?
assert_equal members(:groucho), sponsor.sponsorable
sponsor.sponsorable_type = "Firm"
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal companies(:first_firm), sponsor.sponsorable
end
@@ -1122,7 +1153,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
comment.post_id = 9223372036854775808 # out of range in the bigint
assert_nil comment.post
- assert_not comment.valid?
+ assert_not_predicate comment, :valid?
assert_equal [{ error: :blank }], comment.errors.details[:post]
end
@@ -1142,7 +1173,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
citibank.firm_id = apple.id.to_s
- assert !citibank.association(:firm).stale_target?
+ assert_not_predicate citibank.association(:firm), :stale_target?
end
def test_reflect_the_most_recent_change
diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb
index e096cd4a0b..25d55dc4c9 100644
--- a/activerecord/test/cases/associations/callbacks_test.rb
+++ b/activerecord/test/cases/associations/callbacks_test.rb
@@ -15,7 +15,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
@david = authors(:david)
@thinking = posts(:thinking)
@authorless = posts(:authorless)
- assert @david.post_log.empty?
+ assert_empty @david.post_log
end
def test_adding_macro_callbacks
@@ -96,7 +96,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
def test_has_and_belongs_to_many_add_callback
david = developers(:david)
ar = projects(:active_record)
- assert ar.developers_log.empty?
+ assert_empty ar.developers_log
ar.developers_with_callbacks << david
assert_equal ["before_adding#{david.id}", "after_adding#{david.id}"], ar.developers_log
ar.developers_with_callbacks << david
@@ -122,12 +122,12 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
assert_equal alice, dev
assert_not_nil new_dev
assert new_dev, "record should not have been saved"
- assert_not alice.new_record?
+ assert_not_predicate alice, :new_record?
end
def test_has_and_belongs_to_many_after_add_called_after_save
ar = projects(:active_record)
- assert ar.developers_log.empty?
+ assert_empty ar.developers_log
alice = Developer.new(name: "alice")
ar.developers_with_callbacks << alice
assert_equal "after_adding#{alice.id}", ar.developers_log.last
@@ -143,7 +143,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
david = developers(:david)
jamis = developers(:jamis)
activerecord = projects(:active_record)
- assert activerecord.developers_log.empty?
+ assert_empty activerecord.developers_log
activerecord.developers_with_callbacks.delete(david)
assert_equal ["before_removing#{david.id}", "after_removing#{david.id}"], activerecord.developers_log
@@ -154,7 +154,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
def test_has_and_belongs_to_many_does_not_fire_callbacks_on_clear
activerecord = projects(:active_record)
- assert activerecord.developers_log.empty?
+ assert_empty activerecord.developers_log
if activerecord.developers_with_callbacks.size == 0
activerecord.developers << developers(:david)
activerecord.developers << developers(:jamis)
@@ -163,7 +163,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
end
activerecord.developers_with_callbacks.flat_map { |d| ["before_removing#{d.id}", "after_removing#{d.id}"] }.sort
assert activerecord.developers_with_callbacks.clear
- assert_predicate activerecord.developers_log, :empty?
+ assert_empty activerecord.developers_log
end
def test_has_many_and_belongs_to_many_callbacks_for_save_on_parent
@@ -183,7 +183,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
@david.unchangeable_posts << @authorless
rescue Exception
end
- assert @david.post_log.empty?
+ assert_empty @david.post_log
assert_not_includes @david.unchangeable_posts, @authorless
@david.reload
assert_not_includes @david.unchangeable_posts, @authorless
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index e69cfe5e52..e717621928 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -37,7 +37,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations
authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).to_a
- assert_equal 1, assert_no_queries { authors.size }
+ assert_equal 3, assert_no_queries { authors.size }
assert_equal 10, assert_no_queries { authors[0].comments.size }
end
@@ -136,7 +136,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_eager_association_loading_with_multiple_stis_and_order
- author = Author.all.merge!(includes: { posts: [ :special_comments , :very_special_comment ] }, order: ["authors.name", "comments.body", "very_special_comments_posts.body"], where: "posts.id = 4").first
+ author = Author.all.merge!(includes: { posts: [ :special_comments, :very_special_comment ] }, order: ["authors.name", "comments.body", "very_special_comments_posts.body"], where: "posts.id = 4").first
assert_equal authors(:david), author
assert_no_queries do
author.posts.first.special_comments
diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
index 8754889143..5fca972aee 100644
--- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
+++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
@@ -8,37 +8,81 @@ module Namespaced
class Post < ActiveRecord::Base
self.table_name = "posts"
has_one :tagging, as: :taggable, class_name: "Tagging"
+
+ def self.polymorphic_name
+ sti_name
+ end
end
end
-class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase
+module PolymorphicFullStiClassNamesSharedTest
def setup
+ @old_store_full_sti_class = ActiveRecord::Base.store_full_sti_class
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
+
post = Namespaced::Post.create(title: "Great stuff", body: "This is not", author_id: 1)
@tagging = Tagging.create(taggable: post)
- @old = ActiveRecord::Base.store_full_sti_class
end
def teardown
- ActiveRecord::Base.store_full_sti_class = @old
+ ActiveRecord::Base.store_full_sti_class = @old_store_full_sti_class
+ end
+
+ def test_class_names
+ ActiveRecord::Base.store_full_sti_class = !store_full_sti_class
+ post = Namespaced::Post.find_by_title("Great stuff")
+ assert_nil post.tagging
+
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
+ post = Namespaced::Post.find_by_title("Great stuff")
+ assert_equal @tagging, post.tagging
end
def test_class_names_with_includes
- ActiveRecord::Base.store_full_sti_class = false
+ ActiveRecord::Base.store_full_sti_class = !store_full_sti_class
post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff")
assert_nil post.tagging
- ActiveRecord::Base.store_full_sti_class = true
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff")
assert_equal @tagging, post.tagging
end
def test_class_names_with_eager_load
- ActiveRecord::Base.store_full_sti_class = false
+ ActiveRecord::Base.store_full_sti_class = !store_full_sti_class
post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff")
assert_nil post.tagging
- ActiveRecord::Base.store_full_sti_class = true
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff")
assert_equal @tagging, post.tagging
end
+
+ def test_class_names_with_find_by
+ post = Namespaced::Post.find_by_title("Great stuff")
+
+ ActiveRecord::Base.store_full_sti_class = !store_full_sti_class
+ assert_nil Tagging.find_by(taggable: post)
+
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
+ assert_equal @tagging, Tagging.find_by(taggable: post)
+ end
+end
+
+class PolymorphicFullStiClassNamesTest < ActiveRecord::TestCase
+ include PolymorphicFullStiClassNamesSharedTest
+
+ private
+ def store_full_sti_class
+ true
+ end
+end
+
+class PolymorphicNonFullStiClassNamesTest < ActiveRecord::TestCase
+ include PolymorphicFullStiClassNamesSharedTest
+
+ private
+ def store_full_sti_class
+ false
+ end
end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 559f0e9338..f46be8734b 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -284,7 +284,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_loading_from_an_association_that_has_a_hash_of_conditions
- assert !Author.all.merge!(includes: :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts.empty?
+ assert_not_empty Author.all.merge!(includes: :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts
end
def test_loading_with_no_associations
@@ -427,7 +427,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name
quoted_posts_id = Comment.connection.quote_table_name("posts") + "." + Comment.connection.quote_column_name("id")
assert_nothing_raised do
- Comment.includes(:post).references(:posts).order(quoted_posts_id)
+ Comment.includes(:post).references(:posts).order(Arel.sql(quoted_posts_id))
end
end
@@ -787,7 +787,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
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))
+ assert_equal tag_with_includes.ordered_taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title)
end
def test_eager_has_many_through_multiple_with_order
@@ -874,14 +874,14 @@ class EagerAssociationTest < ActiveRecord::TestCase
posts(:thinking, :sti_comments),
Post.all.merge!(
includes: [:author, :comments], where: { "authors.name" => "David" },
- order: "UPPER(posts.title)", limit: 2, offset: 1
+ order: Arel.sql("UPPER(posts.title)"), limit: 2, offset: 1
).to_a
)
assert_equal(
posts(:sti_post_and_comments, :sti_comments),
Post.all.merge!(
includes: [:author, :comments], where: { "authors.name" => "David" },
- order: "UPPER(posts.title) DESC", limit: 2, offset: 1
+ order: Arel.sql("UPPER(posts.title) DESC"), limit: 2, offset: 1
).to_a
)
end
@@ -891,14 +891,14 @@ class EagerAssociationTest < ActiveRecord::TestCase
posts(:thinking, :sti_comments),
Post.all.merge!(
includes: [:author, :comments], where: { "authors.name" => "David" },
- order: ["UPPER(posts.title)", "posts.id"], limit: 2, offset: 1
+ order: [Arel.sql("UPPER(posts.title)"), "posts.id"], limit: 2, offset: 1
).to_a
)
assert_equal(
posts(:sti_post_and_comments, :sti_comments),
Post.all.merge!(
includes: [:author, :comments], where: { "authors.name" => "David" },
- order: ["UPPER(posts.title) DESC", "posts.id"], limit: 2, offset: 1
+ order: [Arel.sql("UPPER(posts.title) DESC"), "posts.id"], limit: 2, offset: 1
).to_a
)
end
@@ -1073,7 +1073,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_load_with_sti_sharing_association
- assert_queries(2) do #should not do 1 query per subclass
+ assert_queries(2) do # should not do 1 query per subclass
Comment.includes(:post).to_a
end
end
@@ -1218,6 +1218,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
client = assert_queries(2) { Client.preload(:firm).find(c.id) }
assert_no_queries { assert_nil client.firm }
+ assert_equal c.client_of, client.client_of
end
def test_preloading_empty_belongs_to_polymorphic
@@ -1225,6 +1226,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
tagging = assert_queries(2) { Tagging.preload(:taggable).find(t.id) }
assert_no_queries { assert_nil tagging.taggable }
+ assert_equal t.taggable_id, tagging.taggable_id
end
def test_preloading_through_empty_belongs_to
@@ -1444,51 +1446,51 @@ class EagerAssociationTest < ActiveRecord::TestCase
test "preloading readonly association" do
# has-one
firm = Firm.where(id: "1").preload(:readonly_account).first!
- assert firm.readonly_account.readonly?
+ assert_predicate firm.readonly_account, :readonly?
# has_and_belongs_to_many
project = Project.where(id: "2").preload(:readonly_developers).first!
- assert project.readonly_developers.first.readonly?
+ assert_predicate project.readonly_developers.first, :readonly?
# has-many :through
david = Author.where(id: "1").preload(:readonly_comments).first!
- assert david.readonly_comments.first.readonly?
+ assert_predicate david.readonly_comments.first, :readonly?
end
test "eager-loading non-readonly association" do
# has_one
firm = Firm.where(id: "1").eager_load(:account).first!
- assert_not firm.account.readonly?
+ assert_not_predicate firm.account, :readonly?
# has_and_belongs_to_many
project = Project.where(id: "2").eager_load(:developers).first!
- assert_not project.developers.first.readonly?
+ assert_not_predicate project.developers.first, :readonly?
# has_many :through
david = Author.where(id: "1").eager_load(:comments).first!
- assert_not david.comments.first.readonly?
+ assert_not_predicate david.comments.first, :readonly?
# belongs_to
post = Post.where(id: "1").eager_load(:author).first!
- assert_not post.author.readonly?
+ assert_not_predicate post.author, :readonly?
end
test "eager-loading readonly association" do
# has-one
firm = Firm.where(id: "1").eager_load(:readonly_account).first!
- assert firm.readonly_account.readonly?
+ assert_predicate firm.readonly_account, :readonly?
# has_and_belongs_to_many
project = Project.where(id: "2").eager_load(:readonly_developers).first!
- assert project.readonly_developers.first.readonly?
+ assert_predicate project.readonly_developers.first, :readonly?
# has-many :through
david = Author.where(id: "1").eager_load(:readonly_comments).first!
- assert david.readonly_comments.first.readonly?
+ assert_predicate david.readonly_comments.first, :readonly?
# belongs_to
post = Post.where(id: "1").eager_load(:readonly_author).first!
- assert post.readonly_author.readonly?
+ assert_predicate post.readonly_author, :readonly?
end
test "preloading a polymorphic association with references to the associated table" do
@@ -1501,12 +1503,47 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal posts(:welcome), post
end
+ test "eager-loading with a polymorphic association won't work consistently" do
+ assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).to_a }
+ assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).count }
+ assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).exists? }
+ end
+
# CollectionProxy#reader is expensive, so the preloader avoids calling it.
test "preloading has_many_through association avoids calling association.reader" do
ActiveRecord::Associations::HasManyAssociation.any_instance.expects(:reader).never
Author.preload(:readonly_comments).first!
end
+ test "preloading through a polymorphic association doesn't require the association to exist" do
+ sponsors = []
+ assert_queries 5 do
+ sponsors = Sponsor.where(sponsorable_id: 1).preload(sponsorable: [:post, :membership]).to_a
+ end
+ # check the preload worked
+ assert_queries 0 do
+ sponsors.map(&:sponsorable).map { |s| s.respond_to?(:posts) ? s.post.author : s.membership }
+ end
+ end
+
+ test "preloading a regular association through a polymorphic association doesn't require the association to exist on all types" do
+ sponsors = []
+ assert_queries 6 do
+ sponsors = Sponsor.where(sponsorable_id: 1).preload(sponsorable: [{ post: :first_comment }, :membership]).to_a
+ end
+ # check the preload worked
+ assert_queries 0 do
+ sponsors.map(&:sponsorable).map { |s| s.respond_to?(:posts) ? s.post.author : s.membership }
+ end
+ end
+
+ test "preloading a regular association with a typo through a polymorphic association still raises" do
+ # this test contains an intentional typo of first -> fist
+ assert_raises(ActiveRecord::AssociationNotFoundError) do
+ Sponsor.where(sponsorable_id: 1).preload(sponsorable: [{ post: :fist_comment }, :membership]).to_a
+ end
+ end
+
private
def find_all_ordered(klass, include = nil)
klass.order("#{klass.table_name}.#{klass.primary_key}").includes(include).to_a
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 979dd986de..5d9735d98a 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
@@ -88,12 +88,6 @@ class DeveloperWithSymbolClassName < Developer
has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys
end
-ActiveSupport::Deprecation.silence do
- class DeveloperWithConstantClassName < Developer
- has_and_belongs_to_many :projects, class_name: ProjectWithSymbolsForKeys
- end
-end
-
class DeveloperWithExtendOption < Developer
module NamedExtension
def category
@@ -186,11 +180,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_has_and_belongs_to_many
david = Developer.find(1)
- assert !david.projects.empty?
+ assert_not_empty david.projects
assert_equal 2, david.projects.size
active_record = Project.find(1)
- assert !active_record.developers.empty?
+ assert_not_empty active_record.developers
assert_equal 3, active_record.developers.size
assert_includes active_record.developers, david
end
@@ -268,10 +262,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
no_of_projects = Project.count
aredridel = Developer.new("name" => "Aredridel")
aredridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")])
- assert !aredridel.persisted?
- assert !p.persisted?
+ assert_not_predicate aredridel, :persisted?
+ assert_not_predicate p, :persisted?
assert aredridel.save
- assert aredridel.persisted?
+ assert_predicate aredridel, :persisted?
assert_equal no_of_devels + 1, Developer.count
assert_equal no_of_projects + 1, Project.count
assert_equal 2, aredridel.projects.size
@@ -317,14 +311,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_build
devel = Developer.find(1)
proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") }
- assert !devel.projects.loaded?
+ assert_not_predicate devel.projects, :loaded?
assert_equal devel.projects.last, proj
- assert devel.projects.loaded?
+ assert_predicate devel.projects, :loaded?
- assert !proj.persisted?
+ assert_not_predicate proj, :persisted?
devel.save
- assert proj.persisted?
+ assert_predicate proj, :persisted?
assert_equal devel.projects.last, proj
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
end
@@ -332,14 +326,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_new_aliased_to_build
devel = Developer.find(1)
proj = assert_no_queries(ignore_none: false) { devel.projects.new("name" => "Projekt") }
- assert !devel.projects.loaded?
+ assert_not_predicate devel.projects, :loaded?
assert_equal devel.projects.last, proj
- assert devel.projects.loaded?
+ assert_predicate devel.projects, :loaded?
- assert !proj.persisted?
+ assert_not_predicate proj, :persisted?
devel.save
- assert proj.persisted?
+ assert_predicate proj, :persisted?
assert_equal devel.projects.last, proj
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
end
@@ -349,10 +343,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
devel.projects.build(name: "Make bed")
proj2 = devel.projects.build(name: "Lie in it")
assert_equal devel.projects.last, proj2
- assert !proj2.persisted?
+ assert_not_predicate proj2, :persisted?
devel.save
- assert devel.persisted?
- assert proj2.persisted?
+ assert_predicate devel, :persisted?
+ assert_predicate proj2, :persisted?
assert_equal devel.projects.last, proj2
assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated
end
@@ -360,12 +354,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_create
devel = Developer.find(1)
proj = devel.projects.create("name" => "Projekt")
- assert !devel.projects.loaded?
+ assert_not_predicate devel.projects, :loaded?
assert_equal devel.projects.last, proj
- assert !devel.projects.loaded?
+ assert_not_predicate devel.projects, :loaded?
- assert proj.persisted?
+ assert_predicate proj, :persisted?
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
end
@@ -373,14 +367,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
# in Oracle '' is saved as null therefore need to save ' ' in not null column
post = categories(:general).post_with_conditions.build(body: " ")
- assert post.save
- assert_equal "Yet Another Testing Title", post.title
+ assert post.save
+ assert_equal "Yet Another Testing Title", post.title
# in Oracle '' is saved as null therefore need to save ' ' in not null column
another_post = categories(:general).post_with_conditions.create(body: " ")
- assert another_post.persisted?
- assert_equal "Yet Another Testing Title", another_post.title
+ assert_predicate another_post, :persisted?
+ assert_equal "Yet Another Testing Title", another_post.title
end
def test_distinct_after_the_fact
@@ -447,10 +441,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_removing_associations_on_destroy
david = DeveloperWithBeforeDestroyRaise.find(1)
- assert !david.projects.empty?
+ assert_not_empty david.projects
david.destroy
- assert david.projects.empty?
- assert DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1").empty?
+ assert_empty david.projects
+ assert_empty DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1")
end
def test_destroying
@@ -465,7 +459,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id} AND project_id = #{project.id}")
- assert join_records.empty?
+ assert_empty join_records
assert_equal 1, david.reload.projects.size
assert_equal 1, david.projects.reload.size
@@ -481,7 +475,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}")
- assert join_records.empty?
+ assert_empty join_records
assert_equal 0, david.reload.projects.size
assert_equal 0, david.projects.reload.size
@@ -490,23 +484,23 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_destroy_all
david = Developer.find(1)
david.projects.reload
- assert !david.projects.empty?
+ assert_not_empty david.projects
assert_no_difference "Project.count" do
david.projects.destroy_all
end
join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}")
- assert join_records.empty?
+ assert_empty join_records
- assert david.projects.empty?
- assert david.projects.reload.empty?
+ assert_empty david.projects
+ assert_empty david.projects.reload
end
def test_destroy_associations_destroys_multiple_associations
george = parrots(:george)
- assert !george.pirates.empty?
- assert !george.treasures.empty?
+ assert_not_empty george.pirates
+ assert_not_empty george.treasures
assert_no_difference "Pirate.count" do
assert_no_difference "Treasure.count" do
@@ -515,12 +509,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}")
- assert join_records.empty?
- assert george.pirates.reload.empty?
+ assert_empty join_records
+ assert_empty george.pirates.reload
join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}")
- assert join_records.empty?
- assert george.treasures.reload.empty?
+ assert_empty join_records
+ assert_empty george.treasures.reload
end
def test_associations_with_conditions
@@ -553,7 +547,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer = project.developers.first
assert_no_queries(ignore_none: false) do
- assert project.developers.loaded?
+ assert_predicate project.developers, :loaded?
assert_includes project.developers, developer
end
end
@@ -563,19 +557,19 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer = project.developers.first
project.reload
- assert ! project.developers.loaded?
+ assert_not_predicate project.developers, :loaded?
assert_queries(1) do
assert_includes project.developers, developer
end
- assert ! project.developers.loaded?
+ assert_not_predicate project.developers, :loaded?
end
def test_include_returns_false_for_non_matching_record_to_verify_scoping
project = projects(:active_record)
developer = Developer.create name: "Bryan", salary: 50_000
- assert ! project.developers.loaded?
- assert ! project.developers.include?(developer)
+ assert_not_predicate project.developers, :loaded?
+ assert_not project.developers.include?(developer)
end
def test_find_with_merged_options
@@ -668,7 +662,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_includes developer.sym_special_projects, sp
end
- def test_update_attributes_after_push_without_duplicate_join_table_rows
+ def test_update_columns_after_push_without_duplicate_join_table_rows
developer = Developer.new("name" => "Kano")
project = SpecialProject.create("name" => "Special Project")
assert developer.save
@@ -769,9 +763,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_unloaded_associations_does_not_load_them
developer = developers(:david)
- assert !developer.projects.loaded?
+ assert_not_predicate developer.projects, :loaded?
assert_equal projects(:active_record, :action_controller).map(&:id).sort, developer.project_ids.sort
- assert !developer.projects.loaded?
+ assert_not_predicate developer.projects, :loaded?
end
def test_assign_ids
@@ -954,13 +948,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
end
- def test_with_constant_class_name
- assert_nothing_raised do
- developer = DeveloperWithConstantClassName.new
- developer.projects
- end
- end
-
def test_alternate_database
professor = Professor.create(name: "Plum")
course = Course.create(name: "Forensics")
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 6bd11a5d81..bf2e051def 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -57,7 +57,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase
def test_custom_primary_key_on_new_record_should_fetch_with_query
subscriber = Subscriber.new(nick: "webster132")
- assert !subscriber.subscriptions.loaded?
+ assert_not_predicate subscriber.subscriptions, :loaded?
assert_queries 1 do
assert_equal 2, subscriber.subscriptions.size
@@ -68,7 +68,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase
def test_association_primary_key_on_new_record_should_fetch_with_query
author = Author.new(name: "David")
- assert !author.essays.loaded?
+ assert_not_predicate author.essays, :loaded?
assert_queries 1 do
assert_equal 1, author.essays.size
@@ -103,7 +103,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase
def test_blank_custom_primary_key_on_new_record_should_not_run_queries
author = Author.new
- assert !author.essays.loaded?
+ assert_not_predicate author.essays, :loaded?
assert_queries 0 do
assert_equal 0, author.essays.size
@@ -201,7 +201,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
part.reload
assert_nil part.ship
- assert !part.updated_at_changed?
+ assert_not_predicate part, :updated_at_changed?
end
def test_create_from_association_should_respect_default_scope
@@ -444,7 +444,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
new_clients << company.clients_of_firm.build(name: "Another Client III")
end
- assert_not company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
assert_queries(1) do
assert_same new_clients[0], company.clients_of_firm.third
assert_same new_clients[1], company.clients_of_firm.fourth
@@ -464,7 +464,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
new_clients << company.clients_of_firm.build(name: "Another Client III")
end
- assert_not company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
assert_queries(1) do
assert_same new_clients[0], company.clients_of_firm.third!
assert_same new_clients[1], company.clients_of_firm.fourth!
@@ -497,8 +497,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
person = Person.new
person.first_name = "Naruto"
person.references << Reference.new
- person.id = 10
- person.references
person.save!
assert_equal 1, person.references.update_all(favourite: true)
end
@@ -507,8 +505,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
person = Person.new
person.first_name = "Sasuke"
person.references << Reference.new
- person.id = 10
- person.references
person.save!
assert_predicate person.references, :exists?
end
@@ -587,14 +583,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
# taking from unloaded Relation
bob = klass.find(authors(:bob).id)
new_post = bob.posts.build
- assert_not bob.posts.loaded?
+ assert_not_predicate bob.posts, :loaded?
assert_equal [posts(:misc_by_bob)], bob.posts.take(1)
assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2)
assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3)
# taking from loaded Relation
bob.posts.load
- assert bob.posts.loaded?
+ assert_predicate bob.posts, :loaded?
assert_equal [posts(:misc_by_bob)], bob.posts.take(1)
assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2)
assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3)
@@ -713,13 +709,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_find_each
firm = companies(:first_firm)
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries(4) do
firm.clients.find_each(batch_size: 1) { |c| assert_equal firm.id, c.firm_id }
end
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_find_each_with_conditions
@@ -732,13 +728,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_find_in_batches
firm = companies(:first_firm)
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries(2) do
firm.clients.find_in_batches(batch_size: 2) do |clients|
@@ -746,7 +742,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_find_all_sanitized
@@ -831,7 +827,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_find_scoped_grouped_having
- assert_equal 1, authors(:david).popular_grouped_posts.length
+ assert_equal 2, authors(:david).popular_grouped_posts.length
assert_equal 0, authors(:mary).popular_grouped_posts.length
end
@@ -955,20 +951,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_new_aliased_to_build
company = companies(:first_firm)
new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.new("name" => "Another Client") }
- assert !company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
assert_equal "Another Client", new_client.name
- assert !new_client.persisted?
+ assert_not_predicate new_client, :persisted?
assert_equal new_client, company.clients_of_firm.last
end
def test_build
company = companies(:first_firm)
new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") }
- assert !company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
assert_equal "Another Client", new_client.name
- assert !new_client.persisted?
+ assert_not_predicate new_client, :persisted?
assert_equal new_client, company.clients_of_firm.last
end
@@ -982,9 +978,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_collection_not_empty_after_building
company = companies(:first_firm)
- assert_predicate company.contracts, :empty?
+ assert_empty company.contracts
company.contracts.build
- assert_not_predicate company.contracts, :empty?
+ assert_not_empty company.contracts
+ end
+
+ def test_collection_size_with_dirty_target
+ post = posts(:thinking)
+ assert_equal [], post.reader_ids
+ assert_equal 0, post.readers.size
+ post.readers.reset
+ post.readers.build
+ assert_equal [nil], post.reader_ids
+ assert_equal 1, post.readers.size
+ end
+
+ def test_collection_empty_with_dirty_target
+ post = posts(:thinking)
+ assert_equal [], post.reader_ids
+ assert_empty post.readers
+ post.readers.reset
+ post.readers.build
+ assert_equal [nil], post.reader_ids
+ assert_not_empty post.readers
end
def test_collection_size_twice_for_regressions
@@ -1008,7 +1024,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_followed_by_save_does_not_load_target
companies(:first_firm).clients_of_firm.build("name" => "Another Client")
assert companies(:first_firm).save
- assert !companies(:first_firm).clients_of_firm.loaded?
+ assert_not_predicate companies(:first_firm).clients_of_firm, :loaded?
end
def test_build_without_loading_association
@@ -1028,10 +1044,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_via_block
company = companies(:first_firm)
new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build { |client| client.name = "Another Client" } }
- assert !company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
assert_equal "Another Client", new_client.name
- assert !new_client.persisted?
+ assert_not_predicate new_client, :persisted?
assert_equal new_client, company.clients_of_firm.last
end
@@ -1069,7 +1085,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_predicate companies(:first_firm).clients_of_firm, :loaded?
new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client")
- assert new_client.persisted?
+ assert_predicate new_client, :persisted?
assert_equal new_client, companies(:first_firm).clients_of_firm.last
assert_equal new_client, companies(:first_firm).clients_of_firm.reload.last
end
@@ -1082,7 +1098,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_create_followed_by_save_does_not_load_target
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
assert companies(:first_firm).save
- assert !companies(:first_firm).clients_of_firm.loaded?
+ assert_not_predicate companies(:first_firm).clients_of_firm, :loaded?
end
def test_deleting
@@ -1108,7 +1124,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
# option is not given on the association.
ship = Ship.create(name: "Countless", treasures_count: 10)
- assert_not Ship.reflect_on_association(:treasures).has_cached_counter?
+ assert_not_predicate Ship.reflect_on_association(:treasures), :has_cached_counter?
# Count should come from sql count() of treasures rather than treasures_count attribute
assert_equal ship.treasures.size, 0
@@ -1199,7 +1215,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_empty_with_counter_cache
post = posts(:welcome)
assert_queries(0) do
- assert_not post.comments.empty?
+ assert_not_empty post.comments
end
end
@@ -1211,20 +1227,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- def test_calling_update_attributes_on_id_changes_the_counter_cache
+ def test_calling_update_on_id_changes_the_counter_cache
topic = Topic.order("id ASC").first
original_count = topic.replies.to_a.size
assert_equal original_count, topic.replies_count
first_reply = topic.replies.first
- first_reply.update_attributes(parent_id: nil)
+ first_reply.update(parent_id: nil)
assert_equal original_count - 1, topic.reload.replies_count
- first_reply.update_attributes(parent_id: topic.id)
+ first_reply.update(parent_id: topic.id)
assert_equal original_count, topic.reload.replies_count
end
- def test_calling_update_attributes_changing_ids_doesnt_change_counter_cache
+ def test_calling_update_changing_ids_doesnt_change_counter_cache
topic1 = Topic.find(1)
topic2 = Topic.find(3)
original_count1 = topic1.replies.to_a.size
@@ -1233,11 +1249,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
reply1 = topic1.replies.first
reply2 = topic2.replies.first
- reply1.update_attributes(parent_id: topic2.id)
+ reply1.update(parent_id: topic2.id)
assert_equal original_count1 - 1, topic1.reload.replies_count
assert_equal original_count2 + 1, topic2.reload.replies_count
- reply2.update_attributes(parent_id: topic1.id)
+ reply2.update(parent_id: topic1.id)
assert_equal original_count1, topic1.reload.replies_count
assert_equal original_count2, topic2.reload.replies_count
end
@@ -1441,13 +1457,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_creation_respects_hash_condition
ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build
- assert ms_client.save
- assert_equal "Microsoft", ms_client.name
+ assert ms_client.save
+ assert_equal "Microsoft", ms_client.name
another_ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.create
- assert another_ms_client.persisted?
- assert_equal "Microsoft", another_ms_client.name
+ assert_predicate another_ms_client, :persisted?
+ assert_equal "Microsoft", another_ms_client.name
end
def test_clearing_without_initial_access
@@ -1570,7 +1586,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm = companies(:first_firm)
assert_equal 3, firm.clients.size
firm.destroy
- assert Client.all.merge!(where: "firm_id=#{firm.id}").to_a.empty?
+ assert_empty Client.all.merge!(where: "firm_id=#{firm.id}").to_a
end
def test_dependence_for_associations_with_hash_condition
@@ -1633,7 +1649,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm = RestrictedWithExceptionFirm.create!(name: "restrict")
firm.companies.create(name: "child")
- assert !firm.companies.empty?
+ assert_not_empty firm.companies
assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
assert RestrictedWithExceptionFirm.exists?(name: "restrict")
assert firm.companies.exists?(name: "child")
@@ -1643,11 +1659,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm = RestrictedWithErrorFirm.create!(name: "restrict")
firm.companies.create(name: "child")
- assert !firm.companies.empty?
+ assert_not_empty firm.companies
firm.destroy
- assert !firm.errors.empty?
+ assert_not_empty firm.errors
assert_equal "Cannot delete record because dependent companies exist", firm.errors[:base].first
assert RestrictedWithErrorFirm.exists?(name: "restrict")
@@ -1660,11 +1676,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm = RestrictedWithErrorFirm.create!(name: "restrict")
firm.companies.create(name: "child")
- assert !firm.companies.empty?
+ assert_not_empty firm.companies
firm.destroy
- assert !firm.errors.empty?
+ assert_not_empty firm.errors
assert_equal "Cannot delete record because dependent client companies exist", firm.errors[:base].first
assert RestrictedWithErrorFirm.exists?(name: "restrict")
@@ -1716,8 +1732,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
account = Account.new
orig_accounts = firm.accounts.to_a
- assert !account.valid?
- assert !orig_accounts.empty?
+ assert_not_predicate account, :valid?
+ assert_not_empty orig_accounts
error = assert_raise ActiveRecord::RecordNotSaved do
firm.accounts = [account]
end
@@ -1775,9 +1791,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_unloaded_associations_does_not_load_them
company = companies(:first_firm)
- assert !company.clients.loaded?
+ assert_not_predicate company.clients, :loaded?
assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], company.client_ids
- assert !company.clients.loaded?
+ assert_not_predicate company.clients, :loaded?
end
def test_counter_cache_on_unloaded_association
@@ -1842,6 +1858,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
].each { |block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) }
end
+ def test_associations_order_should_be_priority_over_throughs_order
+ david = authors(:david)
+ expected = [12, 10, 9, 8, 7, 6, 5, 3, 2, 1]
+ assert_equal expected, david.comments_desc.map(&:id)
+ assert_equal expected, Author.includes(:comments_desc).find(david.id).comments_desc.map(&:id)
+ end
+
def test_dynamic_find_should_respect_association_order_for_through
assert_equal Comment.find(10), authors(:david).comments_desc.where("comments.type = 'SpecialComment'").first
assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type("SpecialComment")
@@ -1859,7 +1882,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
client = firm.clients.first
assert_no_queries do
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
assert_equal true, firm.clients.include?(client)
end
end
@@ -1869,18 +1892,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
client = firm.clients.first
firm.reload
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries(1) do
assert_equal true, firm.clients.include?(client)
end
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_include_returns_false_for_non_matching_record_to_verify_scoping
firm = companies(:first_firm)
client = Client.create!(name: "Not Associated")
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_equal false, firm.clients.include?(client)
end
@@ -1889,13 +1912,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients.first
firm.clients.second
firm.clients.last
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_first_or_last_on_loaded_association_should_not_fetch_with_query
firm = companies(:first_firm)
firm.clients.load_target
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
assert_no_queries(ignore_none: false) do
firm.clients.first
@@ -1908,7 +1931,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_first_or_last_on_existing_record_with_build_should_load_association
firm = companies(:first_firm)
firm.clients.build(name: "Foo")
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries 1 do
firm.clients.first
@@ -1916,13 +1939,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients.last
end
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
end
def test_calling_first_nth_or_last_on_existing_record_with_create_should_not_load_association
firm = companies(:first_firm)
firm.clients.create(name: "Foo")
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries 3 do
firm.clients.first
@@ -1930,7 +1953,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients.last
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_first_nth_or_last_on_new_record_should_not_run_queries
@@ -1946,14 +1969,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_first_or_last_with_integer_on_association_should_not_load_association
firm = companies(:first_firm)
firm.clients.create(name: "Foo")
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries 2 do
firm.clients.first(2)
firm.clients.last(2)
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_many_should_count_instead_of_loading_association
@@ -1961,7 +1984,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_queries(1) do
firm.clients.many? # use count query
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_many_on_loaded_association_should_not_use_query
@@ -1973,25 +1996,26 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_many_should_defer_to_collection_if_using_a_block
firm = companies(:first_firm)
assert_queries(1) do
- firm.clients.expects(:size).never
- firm.clients.many? { true }
+ assert_not_called(firm.clients, :size) do
+ firm.clients.many? { true }
+ end
end
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
end
def test_calling_many_should_return_false_if_none_or_one
firm = companies(:another_firm)
- assert !firm.clients_like_ms.many?
+ assert_not_predicate firm.clients_like_ms, :many?
assert_equal 0, firm.clients_like_ms.size
firm = companies(:first_firm)
- assert !firm.limited_clients.many?
+ assert_not_predicate firm.limited_clients, :many?
assert_equal 1, firm.limited_clients.size
end
def test_calling_many_should_return_true_if_more_than_one
firm = companies(:first_firm)
- assert firm.clients.many?
+ assert_predicate firm.clients, :many?
assert_equal 3, firm.clients.size
end
@@ -2000,33 +2024,34 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_queries(1) do
firm.clients.none? # use count query
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_none_on_loaded_association_should_not_use_query
firm = companies(:first_firm)
firm.clients.load # force load
- assert_no_queries { assert ! firm.clients.none? }
+ assert_no_queries { assert_not firm.clients.none? }
end
def test_calling_none_should_defer_to_collection_if_using_a_block
firm = companies(:first_firm)
assert_queries(1) do
- firm.clients.expects(:size).never
- firm.clients.none? { true }
+ assert_not_called(firm.clients, :size) do
+ firm.clients.none? { true }
+ end
end
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
end
def test_calling_none_should_return_true_if_none
firm = companies(:another_firm)
- assert firm.clients_like_ms.none?
+ assert_predicate firm.clients_like_ms, :none?
assert_equal 0, firm.clients_like_ms.size
end
def test_calling_none_should_return_false_if_any
firm = companies(:first_firm)
- assert !firm.limited_clients.none?
+ assert_not_predicate firm.limited_clients, :none?
assert_equal 1, firm.limited_clients.size
end
@@ -2035,39 +2060,40 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_queries(1) do
firm.clients.one? # use count query
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_one_on_loaded_association_should_not_use_query
firm = companies(:first_firm)
firm.clients.load # force load
- assert_no_queries { assert ! firm.clients.one? }
+ assert_no_queries { assert_not firm.clients.one? }
end
def test_calling_one_should_defer_to_collection_if_using_a_block
firm = companies(:first_firm)
assert_queries(1) do
- firm.clients.expects(:size).never
- firm.clients.one? { true }
+ assert_not_called(firm.clients, :size) do
+ firm.clients.one? { true }
+ end
end
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
end
def test_calling_one_should_return_false_if_zero
firm = companies(:another_firm)
- assert ! firm.clients_like_ms.one?
+ assert_not_predicate firm.clients_like_ms, :one?
assert_equal 0, firm.clients_like_ms.size
end
def test_calling_one_should_return_true_if_one
firm = companies(:first_firm)
- assert firm.limited_clients.one?
+ assert_predicate firm.limited_clients, :one?
assert_equal 1, firm.limited_clients.size
end
def test_calling_one_should_return_false_if_more_than_one
firm = companies(:first_firm)
- assert ! firm.clients.one?
+ assert_not_predicate firm.clients, :one?
assert_equal 3, firm.clients.size
end
@@ -2089,9 +2115,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_association_proxy_transaction_method_starts_transaction_in_association_class
- Comment.expects(:transaction)
- Post.first.comments.transaction do
- # nothing
+ assert_called(Comment, :transaction) do
+ Post.first.comments.transaction do
+ # nothing
+ end
end
end
@@ -2287,7 +2314,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
assert post.taggings_with_delete_all.count > 0
- assert !post.taggings_with_delete_all.loaded?
+ assert_not_predicate post.taggings_with_delete_all, :loaded?
# 2 queries: one DELETE and another to update the counter cache
assert_queries(2) do
@@ -2309,7 +2336,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
test "collection proxy respects default scope" do
author = authors(:mary)
- assert !author.first_posts.exists?
+ assert_not_predicate author.first_posts, :exists?
end
test "association with extend option" do
@@ -2350,7 +2377,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
test "does not duplicate associations when used with natural primary keys" do
speedometer = Speedometer.create!(id: "4")
- speedometer.minivans.create!(minivan_id: "a-van-red" , name: "a van", color: "red")
+ speedometer.minivans.create!(minivan_id: "a-van-red", name: "a van", color: "red")
assert_equal 1, speedometer.minivans.to_a.size, "Only one association should be present:\n#{speedometer.minivans.to_a}"
assert_equal 1, speedometer.reload.minivans.to_a.size
@@ -2428,7 +2455,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
pirate = FamousPirate.new
pirate.famous_ships << ship = FamousShip.new
- assert pirate.valid?
+ assert_predicate pirate, :valid?
assert_not pirate.valid?(:conference)
assert_equal "can't be blank", ship.errors[:name].first
end
@@ -2509,6 +2536,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_same car, new_bulb.car
end
+ test "reattach to new objects replaces inverse association and foreign key" do
+ bulb = Bulb.create!(car: Car.create!)
+ assert bulb.car_id
+ car = Car.new
+ car.bulbs << bulb
+ assert_equal car, bulb.car
+ assert_nil bulb.car_id
+ end
+
test "in memory replacement maintains order" do
first_bulb = Bulb.create!
second_bulb = Bulb.create!
@@ -2520,6 +2556,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [first_bulb, second_bulb], car.bulbs
end
+ test "association size calculation works with default scoped selects when not previously fetched" do
+ firm = Firm.create!(name: "Firm")
+ 5.times { firm.developers_with_select << Developer.create!(name: "Developer") }
+
+ same_firm = Firm.find(firm.id)
+ assert_equal 5, same_firm.developers_with_select.size
+ end
+
test "prevent double insertion of new object when the parent association loaded in the after save callback" do
reset_callbacks(:save, Bulb) do
Bulb.after_save { |record| record.car.bulbs.load }
@@ -2543,6 +2587,70 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
+ test "calling size on an association that has not been loaded performs a query" do
+ car = Car.create!
+ Bulb.create(car_id: car.id)
+
+ car_two = Car.create!
+
+ assert_queries(1) do
+ assert_equal 1, car.bulbs.size
+ end
+
+ assert_queries(1) do
+ assert_equal 0, car_two.bulbs.size
+ end
+ end
+
+ test "calling size on an association that has been loaded does not perform query" do
+ car = Car.create!
+ Bulb.create(car_id: car.id)
+ car.bulb_ids
+
+ car_two = Car.create!
+ car_two.bulb_ids
+
+ assert_no_queries do
+ assert_equal 1, car.bulbs.size
+ end
+
+ assert_no_queries do
+ assert_equal 0, car_two.bulbs.size
+ end
+ end
+
+ test "calling empty on an association that has not been loaded performs a query" do
+ car = Car.create!
+ Bulb.create(car_id: car.id)
+
+ car_two = Car.create!
+
+ assert_queries(1) do
+ assert_not_empty car.bulbs
+ end
+
+ assert_queries(1) do
+ assert_empty car_two.bulbs
+ end
+ end
+
+ test "calling empty on an association that has been loaded does not performs query" do
+ car = Car.create!
+ Bulb.create(car_id: car.id)
+ car.bulb_ids
+
+ car_two = Car.create!
+ car_two.bulb_ids
+
+ assert_no_queries do
+ assert_not_empty car.bulbs
+ end
+
+ assert_no_queries do
+ assert_empty car_two.bulbs
+ end
+ end
+
class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base
self.table_name = "authors"
has_many :posts_with_error_destroying,
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 521b388cee..0facc286da 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -46,6 +46,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
Reader.create person_id: 0, post_id: 0
end
+ def test_marshal_dump
+ preloaded = Post.includes(:first_blue_tags).first
+ assert_equal preloaded, Marshal.load(Marshal.dump(preloaded))
+ end
+
def test_preload_sti_rhs_class
developers = Developer.includes(:firms).all.to_a
assert_no_queries do
@@ -353,10 +358,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
assert_queries(1) do
- assert posts(:welcome).people.empty?
+ assert_empty posts(:welcome).people
end
- assert posts(:welcome).reload.people.reload.empty?
+ assert_empty posts(:welcome).reload.people.reload
end
def test_destroy_association
@@ -366,8 +371,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
- assert posts(:welcome).reload.people.empty?
- assert posts(:welcome).people.reload.empty?
+ assert_empty posts(:welcome).reload.people
+ assert_empty posts(:welcome).people.reload
end
def test_destroy_all
@@ -377,8 +382,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
- assert posts(:welcome).reload.people.empty?
- assert posts(:welcome).people.reload.empty?
+ assert_empty posts(:welcome).reload.people
+ assert_empty posts(:welcome).people.reload
end
def test_should_raise_exception_for_destroying_mismatching_records
@@ -538,6 +543,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_update_counter_caches_on_destroy_with_indestructible_through_record
+ post = posts(:welcome)
+ tag = post.indestructible_tags.create!(name: "doomed")
+ post.update_columns(indestructible_tags_count: post.indestructible_tags.count)
+
+ assert_no_difference "post.reload.indestructible_tags_count" do
+ posts(:welcome).indestructible_tags.destroy(tag)
+ end
+ end
+
def test_replace_association
assert_queries(4) { posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload }
@@ -675,10 +690,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
assert_queries(0) do
- assert posts(:welcome).people.empty?
+ assert_empty posts(:welcome).people
end
- assert posts(:welcome).reload.people.reload.empty?
+ assert_empty posts(:welcome).reload.people.reload
end
def test_association_callback_ordering
@@ -760,9 +775,9 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_unloaded_associations_does_not_load_them
person = people(:michael)
- assert !person.posts.loaded?
+ assert_not_predicate person.posts, :loaded?
assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort
- assert !person.posts.loaded?
+ assert_not_predicate person.posts, :loaded?
end
def test_association_proxy_transaction_method_starts_transaction_in_association_class
@@ -851,8 +866,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
author = authors(:mary)
category = author.named_categories.create(name: "Primary")
author.named_categories.delete(category)
- assert !Categorization.exists?(author_id: author.id, named_category_name: category.name)
- assert author.named_categories.reload.empty?
+ assert_not Categorization.exists?(author_id: author.id, named_category_name: category.name)
+ assert_empty author.named_categories.reload
end
def test_collection_singular_ids_getter_with_string_primary_keys
@@ -869,6 +884,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [dev], company.developers
end
+ def test_collection_singular_ids_setter_with_required_type_cast
+ company = companies(:rails_core)
+ dev = Developer.first
+
+ company.developer_ids = [dev.id.to_s]
+ assert_equal [dev], company.developers
+ end
+
def test_collection_singular_ids_setter_with_string_primary_keys
assert_nothing_raised do
book = books(:awdr)
@@ -926,8 +949,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_through_association_readonly_should_be_false
- assert !people(:michael).posts.first.readonly?
- assert !people(:michael).posts.to_a.first.readonly?
+ assert_not_predicate people(:michael).posts.first, :readonly?
+ assert_not_predicate people(:michael).posts.to_a.first, :readonly?
end
def test_can_update_through_association
@@ -1016,12 +1039,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post.author_categorizations
proxy = post.send(:association_instance_get, :author_categorizations)
- assert !proxy.stale_target?
+ assert_not_predicate proxy, :stale_target?
assert_equal authors(:mary).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id)
post.author_id = authors(:david).id
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal authors(:david).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id)
end
@@ -1038,7 +1061,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_includes post.author_addresses, address
post.author_addresses.delete(address)
- assert post[:author_count].nil?
+ assert_predicate post[:author_count], :nil?
end
def test_primary_key_option_on_source
@@ -1250,10 +1273,18 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
TenantMembership.current_member = nil
end
+ def test_has_many_through_with_scope_that_has_joined_same_table_with_parent_relation
+ assert_equal authors(:david), Author.joins(:comments_for_first_author).take
+ end
+
def test_has_many_through_with_unscope_should_affect_to_through_scope
assert_equal [comments(:eager_other_comment1)], authors(:mary).unordered_comments
end
+ def test_has_many_through_with_scope_should_accept_string_and_hash_join
+ assert_equal authors(:david), Author.joins({ comments_for_first_author: :post }, "inner join posts posts_alias on authors.id = posts_alias.author_id").eager_load(:categories).take
+ end
+
def test_has_many_through_with_scope_should_respect_table_alias
family = Family.create!
users = 3.times.map { User.create! }
@@ -1292,6 +1323,70 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_has_many_through_update_ids_with_conditions
+ author = Author.create!(name: "Bill")
+ category = categories(:general)
+
+ author.update(
+ special_categories_with_condition_ids: [category.id],
+ nonspecial_categories_with_condition_ids: [category.id]
+ )
+
+ assert_equal [category.id], author.special_categories_with_condition_ids
+ assert_equal [category.id], author.nonspecial_categories_with_condition_ids
+
+ author.update(nonspecial_categories_with_condition_ids: [])
+ author.reload
+
+ assert_equal [category.id], author.special_categories_with_condition_ids
+ assert_equal [], author.nonspecial_categories_with_condition_ids
+ end
+
+ def test_single_has_many_through_association_with_unpersisted_parent_instance
+ post_with_single_has_many_through = Class.new(Post) do
+ def self.name; "PostWithSingleHasManyThrough"; end
+ has_many :subscriptions, through: :author
+ end
+ post = post_with_single_has_many_through.new
+
+ post.author = authors(:mary)
+ book1 = Book.create!(name: "essays on single has many through associations 1")
+ post.author.books << book1
+ subscription1 = Subscription.first
+ book1.subscriptions << subscription1
+ assert_equal [subscription1], post.subscriptions.to_a
+
+ post.author = authors(:bob)
+ book2 = Book.create!(name: "essays on single has many through associations 2")
+ post.author.books << book2
+ subscription2 = Subscription.second
+ book2.subscriptions << subscription2
+ assert_equal [subscription2], post.subscriptions.to_a
+ end
+
+ def test_nested_has_many_through_association_with_unpersisted_parent_instance
+ post_with_nested_has_many_through = Class.new(Post) do
+ def self.name; "PostWithNestedHasManyThrough"; end
+ has_many :books, through: :author
+ has_many :subscriptions, through: :books
+ end
+ post = post_with_nested_has_many_through.new
+
+ post.author = authors(:mary)
+ book1 = Book.create!(name: "essays on nested has many through associations 1")
+ post.author.books << book1
+ subscription1 = Subscription.first
+ book1.subscriptions << subscription1
+ assert_equal [subscription1], post.subscriptions.to_a
+
+ post.author = authors(:bob)
+ book2 = Book.create!(name: "essays on nested has many through associations 2")
+ post.author.books << book2
+ subscription2 = Subscription.second
+ book2.subscriptions << subscription2
+ assert_equal [subscription2], post.subscriptions.to_a
+ end
+
private
def make_model(name)
Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 2a9ebd19ed..d7e898a1c0 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -30,7 +30,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
ActiveRecord::SQLCounter.clear_log
companies(:first_firm).account
ensure
- assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query"
+ log_all = ActiveRecord::SQLCounter.log_all
+ assert log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{log_all}"
end
def test_has_one_cache_nils
@@ -113,8 +114,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
developer = Developer.create!(name: "Someone")
ship = Ship.create!(name: "Planet Caravan", developer: developer)
ship.destroy
- assert !ship.persisted?
- assert !developer.persisted?
+ assert_not_predicate ship, :persisted?
+ assert_not_predicate developer, :persisted?
end
def test_natural_assignment_to_nil_after_destroy
@@ -185,7 +186,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
assert RestrictedWithExceptionFirm.exists?(name: "restrict")
- assert firm.account.present?
+ assert_predicate firm.account, :present?
end
def test_restrict_with_error
@@ -196,10 +197,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
firm.destroy
- assert !firm.errors.empty?
+ assert_not_empty firm.errors
assert_equal "Cannot delete record because a dependent account exists", firm.errors[:base].first
assert RestrictedWithErrorFirm.exists?(name: "restrict")
- assert firm.account.present?
+ assert_predicate firm.account, :present?
end
def test_restrict_with_error_with_locale
@@ -212,10 +213,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
firm.destroy
- assert !firm.errors.empty?
+ assert_not_empty firm.errors
assert_equal "Cannot delete record because a dependent firm account exists", firm.errors[:base].first
assert RestrictedWithErrorFirm.exists?(name: "restrict")
- assert firm.account.present?
+ assert_predicate firm.account, :present?
ensure
I18n.backend.reload!
end
@@ -376,7 +377,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_assignment_before_child_saved
firm = Firm.find(1)
firm.account = a = Account.new("credit_limit" => 1000)
- assert a.persisted?
+ assert_predicate a, :persisted?
assert_equal a, firm.account
assert_equal a, firm.account
firm.association(:account).reload
@@ -394,7 +395,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_cant_save_readonly_association
assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_firm).readonly_account.save! }
- assert companies(:first_firm).readonly_account.readonly?
+ assert_predicate companies(:first_firm).readonly_account, :readonly?
end
def test_has_one_proxy_should_not_respond_to_private_methods
@@ -432,7 +433,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_create_respects_hash_condition
account = companies(:first_firm).create_account_limit_500_with_hash_conditions
- assert account.persisted?
+ assert_predicate account, :persisted?
assert_equal 500, account.credit_limit
end
@@ -449,9 +450,9 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
new_ship = pirate.create_ship
assert_not_equal ships(:black_pearl), new_ship
assert_equal new_ship, pirate.ship
- assert new_ship.new_record?
+ assert_predicate new_ship, :new_record?
assert_nil orig_ship.pirate_id
- assert !orig_ship.changed? # check it was saved
+ assert_not orig_ship.changed? # check it was saved
end
def test_creation_failure_with_dependent_option
@@ -459,8 +460,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
orig_ship = pirate.dependent_ship
new_ship = pirate.create_dependent_ship
- assert new_ship.new_record?
- assert orig_ship.destroyed?
+ assert_predicate new_ship, :new_record?
+ assert_predicate orig_ship, :destroyed?
end
def test_creation_failure_due_to_new_record_should_raise_error
@@ -480,7 +481,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
pirate = pirates(:blackbeard)
pirate.ship.name = nil
- assert !pirate.ship.valid?
+ assert_not_predicate pirate.ship, :valid?
error = assert_raise(ActiveRecord::RecordNotSaved) do
pirate.ship = ships(:interceptor)
end
@@ -587,7 +588,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
ship.save!
ship.name = "new name"
- assert ship.changed?
+ assert_predicate ship, :changed?
assert_queries(1) do
# One query for updating name, not triggering query for updating pirate_id
pirate.ship = ship
@@ -677,7 +678,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
book = SpecialBook.create!(status: "published")
author.book = book
- refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count
+ assert_not_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count
end
def test_association_enum_works_properly_with_nested_join
@@ -724,4 +725,28 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_not DestroyByParentBook.exists?(book.id)
end
+
+ class UndestroyableBook < ActiveRecord::Base
+ self.table_name = "books"
+ belongs_to :author, class_name: "DestroyableAuthor"
+ before_destroy :dont
+
+ def dont
+ throw(:abort)
+ end
+ end
+
+ class DestroyableAuthor < ActiveRecord::Base
+ self.table_name = "authors"
+ has_one :book, class_name: "UndestroyableBook", foreign_key: "author_id", dependent: :destroy
+ end
+
+ def test_dependency_should_halt_parent_destruction
+ author = DestroyableAuthor.create!(name: "Test")
+ UndestroyableBook.create!(author: author)
+
+ assert_no_difference ["DestroyableAuthor.count", "UndestroyableBook.count"] do
+ assert_not author.destroy
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index fe24c465b2..0309663943 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -42,6 +42,18 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_not_nil new_member.club
end
+ def test_creating_association_builds_through_record
+ new_member = Member.create(name: "Chris")
+ new_club = new_member.association(:club).build
+ assert new_member.current_membership
+ assert_equal new_club, new_member.club
+ assert_predicate new_club, :new_record?
+ assert_predicate new_member.current_membership, :new_record?
+ assert new_member.save
+ assert_predicate new_club, :persisted?
+ assert_predicate new_member.current_membership, :persisted?
+ end
+
def test_creating_association_builds_through_record_for_new
new_member = Member.new(name: "Jane")
new_member.club = clubs(:moustache_club)
@@ -52,6 +64,24 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_equal clubs(:moustache_club), new_member.club
end
+ def test_building_multiple_associations_builds_through_record
+ member_type = MemberType.create!
+ member = Member.create!
+ member_detail_with_one_association = MemberDetail.new(member_type: member_type)
+ assert_predicate member_detail_with_one_association.member, :new_record?
+ member_detail_with_two_associations = MemberDetail.new(member_type: member_type, admittable: member)
+ assert_predicate member_detail_with_two_associations.member, :new_record?
+ end
+
+ def test_creating_multiple_associations_creates_through_record
+ member_type = MemberType.create!
+ member = Member.create!
+ member_detail_with_one_association = MemberDetail.create!(member_type: member_type)
+ assert_not_predicate member_detail_with_one_association.member, :new_record?
+ member_detail_with_two_associations = MemberDetail.create!(member_type: member_type, admittable: member)
+ assert_not_predicate member_detail_with_two_associations.member, :new_record?
+ end
+
def test_creating_association_sets_both_parent_ids_for_new
member = Member.new(name: "Sean Griffin")
club = Club.new(name: "Da Club")
@@ -100,7 +130,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_one_through_eager_loading
- members = assert_queries(3) do #base table, through table, clubs table
+ members = assert_queries(3) do # base table, through table, clubs table
Member.all.merge!(includes: :club, where: ["name = ?", "Groucho Marx"]).to_a
end
assert_equal 1, members.size
@@ -108,7 +138,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_one_through_eager_loading_through_polymorphic
- members = assert_queries(3) do #base table, through table, clubs table
+ members = assert_queries(3) do # base table, through table, clubs table
Member.all.merge!(includes: :sponsor_club, where: ["name = ?", "Groucho Marx"]).to_a
end
assert_equal 1, members.size
@@ -139,7 +169,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_nonpreload_eagerloading
members = assert_queries(1) do
- Member.all.merge!(includes: :club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a #force fallback
+ Member.all.merge!(includes: :club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback
end
assert_equal 1, members.size
assert_not_nil assert_no_queries { members[0].club }
@@ -147,7 +177,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_nonpreload_eager_loading_through_polymorphic
members = assert_queries(1) do
- Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a #force fallback
+ Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback
end
assert_equal 1, members.size
assert_not_nil assert_no_queries { members[0].sponsor_club }
@@ -156,7 +186,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record
Sponsor.new(sponsor_club: clubs(:crazy_club), sponsorable: members(:groucho)).save!
members = assert_queries(1) do
- Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name DESC").to_a #force fallback
+ Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name DESC").to_a # force fallback
end
assert_equal 1, members.size
assert_not_nil assert_no_queries { members[0].sponsor_club }
@@ -229,7 +259,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
MemberDetail.all.merge!(includes: :member_type).to_a
end
@new_detail = @member_details[0]
- assert @new_detail.send(:association, :member_type).loaded?
+ assert_predicate @new_detail.send(:association, :member_type), :loaded?
assert_no_queries { @new_detail.member_type }
end
@@ -317,12 +347,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
minivan.dashboard
proxy = minivan.send(:association_instance_get, :dashboard)
- assert !proxy.stale_target?
+ assert_not_predicate proxy, :stale_target?
assert_equal dashboards(:cool_first), minivan.dashboard
minivan.speedometer_id = speedometers(:second).id
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal dashboards(:second), minivan.dashboard
end
@@ -334,7 +364,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
minivan.speedometer_id = speedometers(:second).id
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal dashboards(:second), minivan.dashboard
end
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index 23be344419..ca0620dc3b 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -27,6 +27,24 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
end
end
+ def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations_with_left_outer_joins
+ sql = Person.joins(agents: :agents).left_outer_joins(agents: :agents).to_sql
+ assert_match(/agents_people_4/i, sql)
+ end
+
+ def test_construct_finder_sql_does_not_table_name_collide_with_string_joins
+ sql = Person.joins(:agents).joins("JOIN people agents_people ON agents_people.primary_contact_id = people.id").to_sql
+ assert_match(/agents_people_2/i, sql)
+ end
+
+ def test_construct_finder_sql_does_not_table_name_collide_with_aliased_joins
+ people = Person.arel_table
+ agents = people.alias("agents_people")
+ constraint = agents[:primary_contact_id].eq(people[:id])
+ sql = Person.joins(:agents).joins(agents.create_join(agents, agents.create_on(constraint))).to_sql
+ assert_match(/agents_people_2/i, sql)
+ end
+
def test_construct_finder_sql_ignores_empty_joins_hash
sql = Author.joins({}).to_sql
assert_no_match(/JOIN/i, sql)
@@ -97,19 +115,19 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
scope = Post.joins(:special_comments).where(id: posts(:sti_comments).id)
# The join should match SpecialComment and its subclasses only
- assert scope.where("comments.type" => "Comment").empty?
- assert !scope.where("comments.type" => "SpecialComment").empty?
- assert !scope.where("comments.type" => "SubSpecialComment").empty?
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ assert_not_empty scope.where("comments.type" => "SubSpecialComment")
end
def test_find_with_conditions_on_reflection
- assert !posts(:welcome).comments.empty?
+ assert_not_empty posts(:welcome).comments
assert Post.joins(:nonexistent_comments).where(id: posts(:welcome).id).empty? # [sic!]
end
def test_find_with_conditions_on_through_reflection
- assert !posts(:welcome).tags.empty?
- assert Post.joins(:misc_tags).where(id: posts(:welcome).id).empty?
+ assert_not_empty posts(:welcome).tags
+ assert_empty Post.joins(:misc_tags).where(id: posts(:welcome).id)
end
test "the default scope of the target is applied when joining associations" do
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index e13cf93dcf..896bf574f4 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -129,7 +129,7 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
def test_polymorphic_has_one_should_find_inverse_automatically
man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse)
- assert man_reflection.has_inverse?
+ assert_predicate man_reflection, :has_inverse?
end
end
@@ -150,22 +150,22 @@ class InverseAssociationTests < ActiveRecord::TestCase
def test_should_be_able_to_ask_a_reflection_if_it_has_an_inverse
has_one_with_inverse_ref = Man.reflect_on_association(:face)
- assert has_one_with_inverse_ref.has_inverse?
+ assert_predicate has_one_with_inverse_ref, :has_inverse?
has_many_with_inverse_ref = Man.reflect_on_association(:interests)
- assert has_many_with_inverse_ref.has_inverse?
+ assert_predicate has_many_with_inverse_ref, :has_inverse?
belongs_to_with_inverse_ref = Face.reflect_on_association(:man)
- assert belongs_to_with_inverse_ref.has_inverse?
+ assert_predicate belongs_to_with_inverse_ref, :has_inverse?
has_one_without_inverse_ref = Club.reflect_on_association(:sponsor)
- assert !has_one_without_inverse_ref.has_inverse?
+ assert_not_predicate has_one_without_inverse_ref, :has_inverse?
has_many_without_inverse_ref = Club.reflect_on_association(:memberships)
- assert !has_many_without_inverse_ref.has_inverse?
+ assert_not_predicate has_many_without_inverse_ref, :has_inverse?
belongs_to_without_inverse_ref = Sponsor.reflect_on_association(:sponsor_club)
- assert !belongs_to_without_inverse_ref.has_inverse?
+ assert_not_predicate belongs_to_without_inverse_ref, :has_inverse?
end
def test_inverse_of_method_should_supply_the_actual_reflection_instance_it_is_the_inverse_of
@@ -190,6 +190,16 @@ class InverseAssociationTests < ActiveRecord::TestCase
assert_nil belongs_to_ref.inverse_of
end
+ def test_polymorphic_associations_dont_attempt_to_find_inverse_of
+ belongs_to_ref = Sponsor.reflect_on_association(:sponsor)
+ assert_raise(ArgumentError) { belongs_to_ref.klass }
+ assert_nil belongs_to_ref.inverse_of
+
+ belongs_to_ref = Face.reflect_on_association(:human)
+ assert_raise(ArgumentError) { belongs_to_ref.klass }
+ assert_nil belongs_to_ref.inverse_of
+ end
+
def test_this_inverse_stuff
firm = Firm.create!(name: "Adequate Holdings")
Project.create!(name: "Project 1", firm: firm)
@@ -464,7 +474,7 @@ class InverseHasManyTests < ActiveRecord::TestCase
interest = Interest.create!(man: man)
man.interests.find(interest.id)
- assert_not man.interests.loaded?
+ assert_not_predicate man.interests, :loaded?
end
def test_raise_record_not_found_error_when_invalid_ids_are_passed
@@ -484,7 +494,10 @@ class InverseHasManyTests < ActiveRecord::TestCase
def test_raise_record_not_found_error_when_no_ids_are_passed
man = Man.create!
- assert_raise(ActiveRecord::RecordNotFound) { man.interests.find() }
+ exception = assert_raise(ActiveRecord::RecordNotFound) { man.interests.load.find() }
+
+ assert_equal exception.model, "Interest"
+ assert_equal exception.primary_key, "id"
end
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
@@ -501,16 +514,16 @@ class InverseHasManyTests < ActiveRecord::TestCase
i.man.name = "Charles"
assert_equal i.man.name, man.name
- assert !man.persisted?
+ assert_not_predicate man, :persisted?
end
def test_inverse_instance_should_be_set_before_find_callbacks_are_run
reset_callbacks(Interest, :find) do
Interest.after_find { raise unless association(:man).loaded? && man.present? }
- assert Man.first.interests.reload.any?
- assert Man.includes(:interests).first.interests.any?
- assert Man.joins(:interests).includes(:interests).first.interests.any?
+ assert_predicate Man.first.interests.reload, :any?
+ assert_predicate Man.includes(:interests).first.interests, :any?
+ assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any?
end
end
@@ -518,9 +531,9 @@ class InverseHasManyTests < ActiveRecord::TestCase
reset_callbacks(Interest, :initialize) do
Interest.after_initialize { raise unless association(:man).loaded? && man.present? }
- assert Man.first.interests.reload.any?
- assert Man.includes(:interests).first.interests.any?
- assert Man.joins(:interests).includes(:interests).first.interests.any?
+ assert_predicate Man.first.interests.reload, :any?
+ assert_predicate Man.includes(:interests).first.interests, :any?
+ assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any?
end
end
@@ -672,6 +685,16 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
assert_equal old_inversed_man.object_id, new_inversed_man.object_id
end
+ def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed_with_validation
+ face = Face.new man: Man.new
+
+ old_inversed_man = face.man
+ face.save!
+ new_inversed_man = face.man
+
+ assert_equal old_inversed_man.object_id, new_inversed_man.object_id
+ end
+
def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
i = interests(:llama_wrangling)
m = i.polymorphic_man
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 87694b0788..9d1c73c33b 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -44,11 +44,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_has_many_distinct_through_count
author = authors(:mary)
- assert !authors(:mary).unique_categorized_posts.loaded?
+ assert_not_predicate authors(:mary).unique_categorized_posts, :loaded?
assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count }
assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count(:title) }
assert_queries(1) { assert_equal 0, author.unique_categorized_posts.where(title: nil).count(:title) }
- assert !authors(:mary).unique_categorized_posts.loaded?
+ assert_not_predicate authors(:mary).unique_categorized_posts, :loaded?
end
def test_has_many_distinct_through_find
@@ -369,7 +369,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
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
+ assert_not_equal [], tags(:general).tagged_posts
end
def test_eager_has_many_polymorphic_with_source_type
@@ -454,8 +454,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_has_many_through_uses_conditions_specified_on_the_has_many_association
author = Author.first
- assert author.comments.present?
- assert author.nonexistent_comments.blank?
+ assert_predicate author.comments, :present?
+ assert_predicate author.nonexistent_comments, :blank?
end
def test_has_many_through_uses_correct_attributes
@@ -467,27 +467,27 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
new_tag = Tag.new(name: "new")
saved_post.tags << new_tag
- assert new_tag.persisted? #consistent with habtm!
- assert saved_post.persisted?
+ assert new_tag.persisted? # consistent with habtm!
+ assert_predicate saved_post, :persisted?
assert_includes saved_post.tags, new_tag
- assert new_tag.persisted?
+ assert_predicate new_tag, :persisted?
assert_includes saved_post.reload.tags.reload, new_tag
new_post = Post.new(title: "Association replacement works!", body: "You best believe it.")
saved_tag = tags(:general)
new_post.tags << saved_tag
- assert !new_post.persisted?
- assert saved_tag.persisted?
+ assert_not_predicate new_post, :persisted?
+ assert_predicate saved_tag, :persisted?
assert_includes new_post.tags, saved_tag
new_post.save!
- assert new_post.persisted?
+ assert_predicate new_post, :persisted?
assert_includes new_post.reload.tags.reload, saved_tag
- assert !posts(:thinking).tags.build.persisted?
- assert !posts(:thinking).tags.new.persisted?
+ assert_not_predicate posts(:thinking).tags.build, :persisted?
+ assert_not_predicate posts(:thinking).tags.new, :persisted?
end
def test_create_associate_when_adding_to_has_many_through
@@ -529,14 +529,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_has_many_through_collection_size_doesnt_load_target_if_not_loaded
author = authors(:david)
assert_equal 10, author.comments.size
- assert !author.comments.loaded?
+ assert_not_predicate author.comments, :loaded?
end
def test_has_many_through_collection_size_uses_counter_cache_if_it_exists
c = categories(:general)
c.categorizations_count = 100
assert_equal 100, c.categorizations.size
- assert !c.categorizations.loaded?
+ assert_not_predicate c.categorizations, :loaded?
end
def test_adding_junk_to_has_many_through_should_raise_type_mismatch
@@ -710,7 +710,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
category = david.categories.first
assert_no_queries do
- assert david.categories.loaded?
+ assert_predicate david.categories, :loaded?
assert_includes david.categories, category
end
end
@@ -720,19 +720,19 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
category = david.categories.first
david.reload
- assert ! david.categories.loaded?
+ assert_not_predicate david.categories, :loaded?
assert_queries(1) do
assert_includes david.categories, category
end
- assert ! david.categories.loaded?
+ assert_not_predicate david.categories, :loaded?
end
def test_has_many_through_include_returns_false_for_non_matching_record_to_verify_scoping
david = authors(:david)
category = Category.create!(name: "Not Associated")
- assert ! david.categories.loaded?
- assert ! david.categories.include?(category)
+ assert_not_predicate david.categories, :loaded?
+ assert_not david.categories.include?(category)
end
def test_has_many_through_goes_through_all_sti_classes
diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb
index c95d0425cd..0e54e8c1b0 100644
--- a/activerecord/test/cases/associations/left_outer_join_association_test.rb
+++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb
@@ -5,6 +5,7 @@ require "models/post"
require "models/comment"
require "models/author"
require "models/essay"
+require "models/category"
require "models/categorization"
require "models/person"
@@ -69,15 +70,15 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
scope = Post.left_outer_joins(:special_comments).where(id: posts(:sti_comments).id)
# The join should match SpecialComment and its subclasses only
- assert scope.where("comments.type" => "Comment").empty?
- assert !scope.where("comments.type" => "SpecialComment").empty?
- assert !scope.where("comments.type" => "SubSpecialComment").empty?
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ assert_not_empty scope.where("comments.type" => "SubSpecialComment")
end
def test_does_not_override_select
authors = Author.select("authors.name, #{%{(authors.author_address_id || ' ' || authors.author_address_extra_id) as addr_id}}").left_outer_joins(:posts)
- assert authors.any?
- assert authors.first.respond_to?(:addr_id)
+ assert_predicate authors, :any?
+ assert_respond_to authors.first, :addr_id
end
test "the default scope of the target is applied when joining associations" do
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 3e37e512ca..03ed1c1d47 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -24,6 +24,11 @@ require "models/category"
require "models/categorization"
require "models/membership"
require "models/essay"
+require "models/hotel"
+require "models/department"
+require "models/chef"
+require "models/cake_designer"
+require "models/drink_designer"
class NestedThroughAssociationsTest < ActiveRecord::TestCase
fixtures :authors, :author_addresses, :books, :posts, :subscriptions, :subscribers, :tags, :taggings,
@@ -73,7 +78,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
# This ensures that the polymorphism of taggings is being observed correctly
authors = Author.joins(:tags).where("taggings.taggable_type" => "FakeModel")
- assert authors.empty?
+ assert_empty authors
end
# has_many through
@@ -172,7 +177,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
members = Member.joins(:organization_member_details).
where("member_details.id" => 9)
- assert members.empty?
+ assert_empty members
end
# has_many through
@@ -204,7 +209,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
members = Member.joins(:organization_member_details_2).
where("member_details.id" => 9)
- assert members.empty?
+ assert_empty members
end
# has_many through
@@ -420,9 +425,14 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
# Check the polymorphism of taggings is being observed correctly (in both joins)
authors = Author.joins(:similar_posts).where("taggings.taggable_type" => "FakeModel")
- assert authors.empty?
+ assert_empty authors
authors = Author.joins(:similar_posts).where("taggings_authors_join.taggable_type" => "FakeModel")
- assert authors.empty?
+ assert_empty authors
+ end
+
+ def test_nested_has_many_through_with_scope_on_polymorphic_reflection
+ authors = Author.joins(:ordered_posts).where("posts.id" => posts(:misc_by_bob).id)
+ assert_equal [authors(:mary), authors(:bob)], authors.distinct.sort_by(&:id)
end
def test_has_many_through_with_foreign_key_option_on_through_reflection
@@ -446,9 +456,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
# Ensure STI is respected in the join
scope = Post.joins(:special_comments_ratings).where(id: posts(:sti_comments).id)
- assert scope.where("comments.type" => "Comment").empty?
- assert !scope.where("comments.type" => "SpecialComment").empty?
- assert !scope.where("comments.type" => "SubSpecialComment").empty?
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ assert_not_empty scope.where("comments.type" => "SubSpecialComment")
end
def test_has_many_through_with_sti_on_nested_through_reflection
@@ -456,8 +466,8 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [taggings(:special_comment_rating)], taggings
scope = Post.joins(:special_comments_ratings_taggings).where(id: posts(:sti_comments).id)
- assert scope.where("comments.type" => "Comment").empty?
- assert !scope.where("comments.type" => "SpecialComment").empty?
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
end
def test_nested_has_many_through_writers_should_raise_error
@@ -507,7 +517,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_nested_has_many_through_with_conditions_on_through_associations_preload
- assert Author.where("tags.id" => 100).joins(:misc_post_first_blue_tags).empty?
+ assert_empty Author.where("tags.id" => 100).joins(:misc_post_first_blue_tags)
authors = assert_queries(3) { Author.includes(:misc_post_first_blue_tags).to_a.sort_by(&:id) }
blue = tags(:blue)
@@ -564,9 +574,40 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
c = Categorization.new
c.author = authors(:david)
c.post_taggings.to_a
- assert !c.post_taggings.empty?
+ assert_not_empty c.post_taggings
c.save
- assert !c.post_taggings.empty?
+ assert_not_empty c.post_taggings
+ end
+
+ def test_polymorphic_has_many_through_when_through_association_has_not_loaded
+ cake_designer = CakeDesigner.create!(chef: Chef.new)
+ drink_designer = DrinkDesigner.create!(chef: Chef.new)
+ department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef])
+ Hotel.create!(departments: [department])
+ hotel = Hotel.includes(:cake_designers, :drink_designers).take
+
+ assert_equal [cake_designer], hotel.cake_designers
+ assert_equal [drink_designer], hotel.drink_designers
+ end
+
+ def test_polymorphic_has_many_through_when_through_association_has_already_loaded
+ cake_designer = CakeDesigner.create!(chef: Chef.new)
+ drink_designer = DrinkDesigner.create!(chef: Chef.new)
+ department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef])
+ Hotel.create!(departments: [department])
+ hotel = Hotel.includes(:chefs, :cake_designers, :drink_designers).take
+
+ assert_equal [cake_designer], hotel.cake_designers
+ assert_equal [drink_designer], hotel.drink_designers
+ end
+
+ def test_polymorphic_has_many_through_joined_different_table_twice
+ cake_designer = CakeDesigner.create!(chef: Chef.new)
+ drink_designer = DrinkDesigner.create!(chef: Chef.new)
+ department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef])
+ hotel = Hotel.create!(departments: [department])
+
+ assert_equal hotel, Hotel.joins(:cake_designers, :drink_designers).take
end
private
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index de04221ea6..fce2a7eed1 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -47,7 +47,7 @@ class AssociationsTest < ActiveRecord::TestCase
ship = Ship.create!(name: "The good ship Dollypop")
part = ship.parts.create!(name: "Mast")
part.mark_for_destruction
- assert ship.parts[0].marked_for_destruction?
+ assert_predicate ship.parts[0], :marked_for_destruction?
end
def test_loading_the_association_target_should_load_most_recent_attributes_for_child_records_marked_for_destruction
@@ -119,7 +119,7 @@ class AssociationProxyTest < ActiveRecord::TestCase
david = authors(:david)
david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!"))
- assert !david.posts.loaded?
+ assert_not_predicate david.posts, :loaded?
assert_includes david.posts, post
end
@@ -127,7 +127,7 @@ class AssociationProxyTest < ActiveRecord::TestCase
david = authors(:david)
david.categories << categories(:technology)
- assert !david.categories.loaded?
+ assert_not_predicate david.categories, :loaded?
assert_includes david.categories, categories(:technology)
end
@@ -135,23 +135,23 @@ class AssociationProxyTest < ActiveRecord::TestCase
david = authors(:david)
david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!"))
- assert !david.posts.loaded?
+ assert_not_predicate david.posts, :loaded?
david.save
- assert !david.posts.loaded?
+ assert_not_predicate david.posts, :loaded?
assert_includes david.posts, post
end
def test_push_does_not_lose_additions_to_new_record
josh = Author.new(name: "Josh")
josh.posts << Post.new(title: "New on Edge", body: "More cool stuff!")
- assert josh.posts.loaded?
+ assert_predicate josh.posts, :loaded?
assert_equal 1, josh.posts.size
end
def test_append_behaves_like_push
josh = Author.new(name: "Josh")
josh.posts.append Post.new(title: "New on Edge", body: "More cool stuff!")
- assert josh.posts.loaded?
+ assert_predicate josh.posts, :loaded?
assert_equal 1, josh.posts.size
end
@@ -163,22 +163,22 @@ class AssociationProxyTest < ActiveRecord::TestCase
def test_save_on_parent_does_not_load_target
david = developers(:david)
- assert !david.projects.loaded?
+ assert_not_predicate david.projects, :loaded?
david.update_columns(created_at: Time.now)
- assert !david.projects.loaded?
+ assert_not_predicate david.projects, :loaded?
end
def test_load_does_load_target
david = developers(:david)
- assert !david.projects.loaded?
+ assert_not_predicate david.projects, :loaded?
david.projects.load
- assert david.projects.loaded?
+ assert_predicate david.projects, :loaded?
end
def test_inspect_does_not_reload_a_not_yet_loaded_target
andreas = Developer.new name: "Andreas", log: "new developer added"
- assert !andreas.audit_logs.loaded?
+ assert_not_predicate andreas.audit_logs, :loaded?
assert_match(/message: "new developer added"/, andreas.audit_logs.inspect)
end
@@ -248,14 +248,14 @@ class AssociationProxyTest < ActiveRecord::TestCase
test "first! works on loaded associations" do
david = authors(:david)
assert_equal david.first_posts.first, david.first_posts.reload.first!
- assert david.first_posts.loaded?
+ assert_predicate david.first_posts, :loaded?
assert_no_queries { david.first_posts.first! }
end
def test_pluck_uses_loaded_target
david = authors(:david)
assert_equal david.first_posts.pluck(:title), david.first_posts.load.pluck(:title)
- assert david.first_posts.loaded?
+ assert_predicate david.first_posts, :loaded?
assert_no_queries { david.first_posts.pluck(:title) }
end
@@ -263,9 +263,9 @@ class AssociationProxyTest < ActiveRecord::TestCase
david = authors(:david)
david.posts.reload
- assert david.posts.loaded?
+ assert_predicate david.posts, :loaded?
david.posts.reset
- assert !david.posts.loaded?
+ assert_not_predicate david.posts, :loaded?
end
end
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index 0170a6e98d..54512068ee 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -12,7 +12,7 @@ module ActiveRecord
def setup
@klass = Class.new(Class.new { def self.initialize_generated_modules; end }) do
def self.superclass; Base; end
- def self.base_class; self; end
+ def self.base_class?; true; end
def self.decorate_matching_attribute_types(*); end
include ActiveRecord::DefineCallbacks
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 0ea8ef5cea..1a37ad963f 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -63,8 +63,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase
t.author_name = ""
assert t.attribute_present?("title")
assert t.attribute_present?("written_on")
- assert !t.attribute_present?("content")
- assert !t.attribute_present?("author_name")
+ assert_not t.attribute_present?("content")
+ assert_not t.attribute_present?("author_name")
end
test "attribute_present with booleans" do
@@ -77,7 +77,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert b2.attribute_present?(:value)
b3 = Boolean.new
- assert !b3.attribute_present?(:value)
+ assert_not b3.attribute_present?(:value)
b4 = Boolean.new
b4.value = false
@@ -99,8 +99,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
test "boolean attributes" do
- assert !Topic.find(1).approved?
- assert Topic.find(2).approved?
+ assert_not_predicate Topic.find(1), :approved?
+ assert_predicate Topic.find(2), :approved?
end
test "set attributes" do
@@ -142,16 +142,16 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_respond_to topic, :title=
assert_respond_to topic, "author_name"
assert_respond_to topic, "attribute_names"
- assert !topic.respond_to?("nothingness")
- assert !topic.respond_to?(:nothingness)
+ assert_not_respond_to topic, "nothingness"
+ assert_not_respond_to topic, :nothingness
end
test "respond_to? with a custom primary key" do
keyboard = Keyboard.create
assert_not_nil keyboard.key_number
assert_equal keyboard.key_number, keyboard.id
- assert keyboard.respond_to?("key_number")
- assert keyboard.respond_to?("id")
+ assert_respond_to keyboard, "key_number"
+ assert_respond_to keyboard, "id"
end
test "id_before_type_cast with a custom primary key" do
@@ -170,8 +170,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
topic = klass.allocate
- assert !topic.respond_to?("nothingness")
- assert !topic.respond_to?(:nothingness)
+ assert_not_respond_to topic, "nothingness"
+ assert_not_respond_to topic, :nothingness
assert_respond_to topic, "title"
assert_respond_to topic, :title
end
@@ -200,12 +200,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
if current_adapter?(:Mysql2Adapter)
test "read attributes_before_type_cast on a boolean" do
bool = Boolean.create!("value" => false)
- if RUBY_PLATFORM.include?("java")
- # JRuby will return the value before typecast as string.
- assert_equal "0", bool.reload.attributes_before_type_cast["value"]
- else
- assert_equal 0, bool.reload.attributes_before_type_cast["value"]
- end
+ assert_equal 0, bool.reload.attributes_before_type_cast["value"]
end
end
@@ -462,30 +457,30 @@ class AttributeMethodsTest < ActiveRecord::TestCase
SQL
assert_equal "Firm", object.string_value
- assert object.string_value?
+ assert_predicate object, :string_value?
object.string_value = " "
- assert !object.string_value?
+ assert_not_predicate object, :string_value?
assert_equal 1, object.int_value.to_i
- assert object.int_value?
+ assert_predicate object, :int_value?
object.int_value = "0"
- assert !object.int_value?
+ assert_not_predicate object, :int_value?
end
test "non-attribute read and write" do
topic = Topic.new
- assert !topic.respond_to?("mumbo")
+ assert_not_respond_to topic, "mumbo"
assert_raise(NoMethodError) { topic.mumbo }
assert_raise(NoMethodError) { topic.mumbo = 5 }
end
test "undeclared attribute method does not affect respond_to? and method_missing" do
topic = @target.new(title: "Budget")
- assert topic.respond_to?("title")
+ assert_respond_to topic, "title"
assert_equal "Budget", topic.title
- assert !topic.respond_to?("title_hello_world")
+ assert_not_respond_to topic, "title_hello_world"
assert_raise(NoMethodError) { topic.title_hello_world }
end
@@ -496,7 +491,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
@target.attribute_method_prefix prefix
meth = "#{prefix}title"
- assert topic.respond_to?(meth)
+ assert_respond_to topic, meth
assert_equal ["title"], topic.send(meth)
assert_equal ["title", "a"], topic.send(meth, "a")
assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3)
@@ -510,7 +505,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
topic = @target.new(title: "Budget")
meth = "title#{suffix}"
- assert topic.respond_to?(meth)
+ assert_respond_to topic, meth
assert_equal ["title"], topic.send(meth)
assert_equal ["title", "a"], topic.send(meth, "a")
assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3)
@@ -524,7 +519,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
topic = @target.new(title: "Budget")
meth = "#{prefix}title#{suffix}"
- assert topic.respond_to?(meth)
+ assert_respond_to topic, meth
assert_equal ["title"], topic.send(meth)
assert_equal ["title", "a"], topic.send(meth, "a")
assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3)
@@ -546,7 +541,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
else
topic = Topic.all.merge!(select: "topics.*, 1=2 as is_test").first
end
- assert !topic.is_test?
+ assert_not_predicate topic, :is_test?
end
test "typecast attribute from select to true" do
@@ -557,7 +552,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
else
topic = Topic.all.merge!(select: "topics.*, 2=2 as is_test").first
end
- assert topic.is_test?
+ assert_predicate topic, :is_test?
end
test "raises ActiveRecord::DangerousAttributeError when defining an AR method in a model" do
@@ -748,7 +743,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
expected_time = Time.utc(2000, 01, 01, 10)
assert_equal expected_time, record.bonus_time
- assert record.bonus_time.utc?
+ assert_predicate record.bonus_time, :utc?
end
end
end
@@ -772,7 +767,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
privatize("title")
topic = @target.new(title: "The pros and cons of programming naked.")
- assert !topic.respond_to?(:title)
+ assert_not_respond_to topic, :title
exception = assert_raise(NoMethodError) { topic.title }
assert_includes exception.message, "private method"
assert_equal "I'm private", topic.send(:title)
@@ -782,7 +777,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
privatize("title=(value)")
topic = @target.new
- assert !topic.respond_to?(:title=)
+ assert_not_respond_to topic, :title=
exception = assert_raise(NoMethodError) { topic.title = "Pants" }
assert_includes exception.message, "private method"
topic.send(:title=, "Very large pants")
@@ -792,7 +787,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
privatize("title?")
topic = @target.new(title: "Isaac Newton's pants")
- assert !topic.respond_to?(:title?)
+ assert_not_respond_to topic, :title?
exception = assert_raise(NoMethodError) { topic.title? }
assert_includes exception.message, "private method"
assert topic.send(:title?)
@@ -832,7 +827,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
self.table_name = "computers"
end
- assert !klass.instance_method_already_implemented?(:system)
+ assert_not klass.instance_method_already_implemented?(:system)
computer = klass.new
assert_nil computer.system
end
@@ -846,8 +841,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase
self.table_name = "computers"
end
- assert !klass.instance_method_already_implemented?(:system)
- assert !subklass.instance_method_already_implemented?(:system)
+ assert_not klass.instance_method_already_implemented?(:system)
+ assert_not subklass.instance_method_already_implemented?(:system)
computer = subklass.new
assert_nil computer.system
end
@@ -984,9 +979,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase
test "came_from_user?" do
model = @target.first
- assert_not model.id_came_from_user?
+ assert_not_predicate model, :id_came_from_user?
model.id = "omg"
- assert model.id_came_from_user?
+ assert_predicate model, :id_came_from_user?
end
test "accessed_fields" do
@@ -999,6 +994,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal ["title"], model.accessed_fields
end
+ test "generated attribute methods ancestors have correct class" do
+ mod = Topic.send(:generated_attribute_methods)
+ assert_match %r(GeneratedAttributeMethods), mod.inspect
+ end
+
private
def new_topic_like_ar_class(&block)
@@ -1019,14 +1019,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
ActiveRecord::Base.time_zone_aware_types = old_types
end
- def cached_columns
- Topic.columns.map(&:name)
- end
-
- def time_related_columns_on_topic
- Topic.columns.select { |c| [:time, :date, :datetime, :timestamp].include?(c.type) }
- end
-
def privatize(method_signature)
@target.class_eval(<<-private_method, __FILE__, __LINE__ + 1)
private
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
index 2caf2a63d4..3bc56694be 100644
--- a/activerecord/test/cases/attributes_test.rb
+++ b/activerecord/test/cases/attributes_test.rb
@@ -59,7 +59,7 @@ module ActiveRecord
test "nonexistent attribute" do
data = OverloadedType.new(non_existent_decimal: 1)
- assert_equal BigDecimal.new(1), data.non_existent_decimal
+ assert_equal BigDecimal(1), data.non_existent_decimal
assert_raise ActiveRecord::UnknownAttributeError do
UnoverloadedType.new(non_existent_decimal: 1)
end
@@ -211,7 +211,7 @@ module ActiveRecord
end
test "attributes not backed by database columns are not dirty when unchanged" do
- refute OverloadedType.new.non_existent_decimal_changed?
+ assert_not_predicate OverloadedType.new, :non_existent_decimal_changed?
end
test "attributes not backed by database columns are always initialized" do
@@ -245,13 +245,13 @@ module ActiveRecord
model.foo << "asdf"
assert_equal "lolasdf", model.foo
- assert model.foo_changed?
+ assert_predicate model, :foo_changed?
model.reload
assert_equal "lol", model.foo
model.foo = "lol"
- refute model.changed?
+ assert_not_predicate model, :changed?
end
test "attributes not backed by database columns appear in inspect" do
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 4d8368fd8a..0c212ecc22 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -50,8 +50,8 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
}
u = person.create!(first_name: "cool")
- u.update_attributes!(first_name: "nah") # still valid because validation only applies on 'create'
- assert reference.create!(person: u).persisted?
+ u.update!(first_name: "nah") # still valid because validation only applies on 'create'
+ assert_predicate reference.create!(person: u), :persisted?
end
def test_should_not_add_the_same_callbacks_multiple_times_for_has_one
@@ -74,7 +74,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
ship = ShipWithoutNestedAttributes.new
ship.prisoners.build
- assert_not ship.valid?
+ assert_not_predicate ship, :valid?
assert_equal 1, ship.errors[:name].length
end
@@ -99,35 +99,35 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
def test_should_save_parent_but_not_invalid_child
firm = Firm.new(name: "GlobalMegaCorp")
- assert firm.valid?
+ assert_predicate firm, :valid?
firm.build_account_using_primary_key
- assert !firm.build_account_using_primary_key.valid?
+ assert_not_predicate firm.build_account_using_primary_key, :valid?
assert firm.save
- assert !firm.account_using_primary_key.persisted?
+ assert_not_predicate firm.account_using_primary_key, :persisted?
end
def test_save_fails_for_invalid_has_one
firm = Firm.first
- assert firm.valid?
+ assert_predicate firm, :valid?
firm.build_account
- assert !firm.account.valid?
- assert !firm.valid?
- assert !firm.save
+ assert_not_predicate firm.account, :valid?
+ assert_not_predicate firm, :valid?
+ assert_not firm.save
assert_equal ["is invalid"], firm.errors["account"]
end
def test_save_succeeds_for_invalid_has_one_with_validate_false
firm = Firm.first
- assert firm.valid?
+ assert_predicate firm, :valid?
firm.build_unvalidated_account
- assert !firm.unvalidated_account.valid?
- assert firm.valid?
+ assert_not_predicate firm.unvalidated_account, :valid?
+ assert_predicate firm, :valid?
assert firm.save
end
@@ -136,10 +136,10 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
account = firm.build_account("credit_limit" => 1000)
assert_equal account, firm.account
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
assert firm.save
assert_equal account, firm.account
- assert account.persisted?
+ assert_predicate account, :persisted?
end
def test_build_before_either_saved
@@ -147,16 +147,16 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
firm.account = account = Account.new("credit_limit" => 1000)
assert_equal account, firm.account
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
assert firm.save
assert_equal account, firm.account
- assert account.persisted?
+ assert_predicate account, :persisted?
end
def test_assignment_before_parent_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.find(1)
- assert !firm.persisted?
+ assert_not_predicate firm, :persisted?
assert_equal a, firm.account
assert firm.save
assert_equal a, firm.account
@@ -167,12 +167,12 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
def test_assignment_before_either_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.new("credit_limit" => 1000)
- assert !firm.persisted?
- assert !a.persisted?
+ assert_not_predicate firm, :persisted?
+ assert_not_predicate a, :persisted?
assert_equal a, firm.account
assert firm.save
- assert firm.persisted?
- assert a.persisted?
+ assert_predicate firm, :persisted?
+ assert_predicate a, :persisted?
assert_equal a, firm.account
firm.association(:account).reload
assert_equal a, firm.account
@@ -221,13 +221,13 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
def test_should_save_parent_but_not_invalid_child
client = Client.new(name: "Joe (the Plumber)")
- assert client.valid?
+ assert_predicate client, :valid?
client.build_firm
- assert !client.firm.valid?
+ assert_not_predicate client.firm, :valid?
assert client.save
- assert !client.firm.persisted?
+ assert_not_predicate client.firm, :persisted?
end
def test_save_fails_for_invalid_belongs_to
@@ -235,9 +235,9 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
assert log = AuditLog.create(developer_id: 0, message: " ")
log.developer = Developer.new
- assert !log.developer.valid?
- assert !log.valid?
- assert !log.save
+ assert_not_predicate log.developer, :valid?
+ assert_not_predicate log, :valid?
+ assert_not log.save
assert_equal ["is invalid"], log.errors["developer"]
end
@@ -246,8 +246,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
assert log = AuditLog.create(developer_id: 0, message: " ")
log.unvalidated_developer = Developer.new
- assert !log.unvalidated_developer.valid?
- assert log.valid?
+ assert_not_predicate log.unvalidated_developer, :valid?
+ assert_predicate log, :valid?
assert log.save
end
@@ -256,10 +256,10 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
apple = Firm.new("name" => "Apple")
client.firm = apple
assert_equal apple, client.firm
- assert !apple.persisted?
+ assert_not_predicate apple, :persisted?
assert client.save
assert apple.save
- assert apple.persisted?
+ assert_predicate apple, :persisted?
assert_equal apple, client.firm
client.association(:firm).reload
assert_equal apple, client.firm
@@ -269,11 +269,11 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
final_cut = Client.new("name" => "Final Cut")
apple = Firm.new("name" => "Apple")
final_cut.firm = apple
- assert !final_cut.persisted?
- assert !apple.persisted?
+ assert_not_predicate final_cut, :persisted?
+ assert_not_predicate apple, :persisted?
assert final_cut.save
- assert final_cut.persisted?
- assert apple.persisted?
+ assert_predicate final_cut, :persisted?
+ assert_predicate apple, :persisted?
assert_equal apple, final_cut.firm
final_cut.association(:firm).reload
assert_equal apple, final_cut.firm
@@ -382,7 +382,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
auditlog.developer = invalid_developer
auditlog.developer_id = valid_developer.id
- assert auditlog.valid?
+ assert_predicate auditlog, :valid?
end
end
@@ -395,8 +395,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
molecule.electrons = [valid_electron, invalid_electron]
molecule.save
- assert_not invalid_electron.valid?
- assert valid_electron.valid?
+ assert_not_predicate invalid_electron, :valid?
+ assert_predicate valid_electron, :valid?
assert_not molecule.persisted?, "Molecule should not be persisted when its electrons are invalid"
end
@@ -408,9 +408,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid]
- assert_not tuning_peg_invalid.valid?
- assert tuning_peg_valid.valid?
- assert_not guitar.valid?
+ assert_not_predicate tuning_peg_invalid, :valid?
+ assert_predicate tuning_peg_valid, :valid?
+ assert_not_predicate guitar, :valid?
assert_equal ["is not a number"], guitar.errors["tuning_pegs[1].pitch"]
assert_not_equal ["is not a number"], guitar.errors["tuning_pegs.pitch"]
end
@@ -425,9 +425,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
molecule.electrons = [valid_electron, invalid_electron]
- assert_not invalid_electron.valid?
- assert valid_electron.valid?
- assert_not molecule.valid?
+ assert_not_predicate invalid_electron, :valid?
+ assert_predicate valid_electron, :valid?
+ assert_not_predicate molecule, :valid?
assert_equal ["can't be blank"], molecule.errors["electrons[1].name"]
assert_not_equal ["can't be blank"], molecule.errors["electrons.name"]
ensure
@@ -441,9 +441,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
molecule.electrons = [valid_electron, invalid_electron]
- assert_not invalid_electron.valid?
- assert valid_electron.valid?
- assert_not molecule.valid?
+ assert_not_predicate invalid_electron, :valid?
+ assert_predicate valid_electron, :valid?
+ assert_not_predicate molecule, :valid?
assert_equal [{ error: :blank }], molecule.errors.details[:"electrons.name"]
end
@@ -455,9 +455,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid]
- assert_not tuning_peg_invalid.valid?
- assert tuning_peg_valid.valid?
- assert_not guitar.valid?
+ assert_not_predicate tuning_peg_invalid, :valid?
+ assert_predicate tuning_peg_valid, :valid?
+ assert_not_predicate guitar, :valid?
assert_equal [{ error: :not_a_number, value: nil }], guitar.errors.details[:"tuning_pegs[1].pitch"]
assert_equal [], guitar.errors.details[:"tuning_pegs.pitch"]
end
@@ -472,9 +472,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
molecule.electrons = [valid_electron, invalid_electron]
- assert_not invalid_electron.valid?
- assert valid_electron.valid?
- assert_not molecule.valid?
+ assert_not_predicate invalid_electron, :valid?
+ assert_predicate valid_electron, :valid?
+ assert_not_predicate molecule, :valid?
assert_equal [{ error: :blank }], molecule.errors.details[:"electrons[1].name"]
assert_equal [], molecule.errors.details[:"electrons.name"]
ensure
@@ -488,8 +488,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
molecule.electrons = [valid_electron]
molecule.save
- assert valid_electron.valid?
- assert molecule.persisted?
+ assert_predicate valid_electron, :valid?
+ assert_predicate molecule, :persisted?
assert_equal 1, molecule.electrons.count
end
end
@@ -499,22 +499,22 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
def test_invalid_adding
firm = Firm.find(1)
- assert !(firm.clients_of_firm << c = Client.new)
- assert !c.persisted?
- assert !firm.valid?
- assert !firm.save
- assert !c.persisted?
+ assert_not (firm.clients_of_firm << c = Client.new)
+ assert_not_predicate c, :persisted?
+ assert_not_predicate firm, :valid?
+ assert_not firm.save
+ assert_not_predicate c, :persisted?
end
def test_invalid_adding_before_save
new_firm = Firm.new("name" => "A New Firm, Inc")
new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
- assert !c.persisted?
- assert !c.valid?
- assert !new_firm.valid?
- assert !new_firm.save
- assert !c.persisted?
- assert !new_firm.persisted?
+ assert_not_predicate c, :persisted?
+ assert_not_predicate c, :valid?
+ assert_not_predicate new_firm, :valid?
+ assert_not new_firm.save
+ assert_not_predicate c, :persisted?
+ assert_not_predicate new_firm, :persisted?
end
def test_invalid_adding_with_validate_false
@@ -522,10 +522,10 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
client = Client.new
firm.unvalidated_clients_of_firm << client
- assert firm.valid?
- assert !client.valid?
+ assert_predicate firm, :valid?
+ assert_not_predicate client, :valid?
assert firm.save
- assert !client.persisted?
+ assert_not_predicate client, :persisted?
end
def test_valid_adding_with_validate_false
@@ -534,24 +534,24 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
firm = Firm.first
client = Client.new("name" => "Apple")
- assert firm.valid?
- assert client.valid?
- assert !client.persisted?
+ assert_predicate firm, :valid?
+ assert_predicate client, :valid?
+ assert_not_predicate client, :persisted?
firm.unvalidated_clients_of_firm << client
assert firm.save
- assert client.persisted?
+ assert_predicate client, :persisted?
assert_equal no_of_clients + 1, Client.count
end
def test_invalid_build
new_client = companies(:first_firm).clients_of_firm.build
- assert !new_client.persisted?
- assert !new_client.valid?
+ assert_not_predicate new_client, :persisted?
+ assert_not_predicate new_client, :valid?
assert_equal new_client, companies(:first_firm).clients_of_firm.last
- assert !companies(:first_firm).save
- assert !new_client.persisted?
+ assert_not companies(:first_firm).save
+ assert_not_predicate new_client, :persisted?
assert_equal 2, companies(:first_firm).clients_of_firm.reload.size
end
@@ -570,8 +570,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
assert_equal no_of_firms, Firm.count # Firm was not saved to database.
assert_equal no_of_clients, Client.count # Clients were not saved to database.
assert new_firm.save
- assert new_firm.persisted?
- assert c.persisted?
+ assert_predicate new_firm, :persisted?
+ assert_predicate c, :persisted?
assert_equal new_firm, c.firm
assert_equal no_of_firms + 1, Firm.count # Firm was saved to database.
assert_equal no_of_clients + 2, Client.count # Clients were saved to database.
@@ -601,11 +601,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
def test_build_before_save
company = companies(:first_firm)
new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") }
- assert !company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
company.name += "-changed"
assert_queries(2) { assert company.save }
- assert new_client.persisted?
+ assert_predicate new_client, :persisted?
assert_equal 3, company.clients_of_firm.reload.size
end
@@ -621,11 +621,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
def test_build_via_block_before_save
company = companies(:first_firm)
new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build { |client| client.name = "Another Client" } }
- assert !company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
company.name += "-changed"
assert_queries(2) { assert company.save }
- assert new_client.persisted?
+ assert_predicate new_client, :persisted?
assert_equal 3, company.clients_of_firm.reload.size
end
@@ -657,62 +657,62 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase
new_account = Account.new("credit_limit" => 1000)
new_firm = Firm.new("name" => "some firm")
- assert !new_firm.persisted?
+ assert_not_predicate new_firm, :persisted?
new_account.firm = new_firm
new_account.save!
- assert new_firm.persisted?
+ assert_predicate new_firm, :persisted?
new_account = Account.new("credit_limit" => 1000)
new_autosaved_firm = Firm.new("name" => "some firm")
- assert !new_autosaved_firm.persisted?
+ assert_not_predicate new_autosaved_firm, :persisted?
new_account.unautosaved_firm = new_autosaved_firm
new_account.save!
- assert !new_autosaved_firm.persisted?
+ assert_not_predicate new_autosaved_firm, :persisted?
end
def test_autosave_new_record_on_has_one_can_be_disabled_per_relationship
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
firm.account = account
firm.save!
- assert account.persisted?
+ assert_predicate account, :persisted?
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
firm.unautosaved_account = account
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
firm.unautosaved_account = account
firm.save!
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
end
def test_autosave_new_record_on_has_many_can_be_disabled_per_relationship
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
firm.accounts << account
firm.save!
- assert account.persisted?
+ assert_predicate account, :persisted?
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
firm.unautosaved_accounts << account
firm.save!
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
end
def test_autosave_new_record_with_after_create_callback
@@ -745,18 +745,18 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.mark_for_destruction
@pirate.ship.mark_for_destruction
- assert !@pirate.reload.marked_for_destruction?
- assert !@pirate.ship.reload.marked_for_destruction?
+ assert_not_predicate @pirate.reload, :marked_for_destruction?
+ assert_not_predicate @pirate.ship.reload, :marked_for_destruction?
end
# has_one
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?
+ assert_not_predicate @pirate.ship, :marked_for_destruction?
@pirate.ship.mark_for_destruction
id = @pirate.ship.id
- assert @pirate.ship.marked_for_destruction?
+ assert_predicate @pirate.ship, :marked_for_destruction?
assert Ship.find_by_id(id)
@pirate.save
@@ -766,11 +766,12 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
def test_should_skip_validation_on_a_child_association_if_marked_for_destruction
@pirate.ship.name = ""
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
@pirate.ship.mark_for_destruction
- @pirate.ship.expects(:valid?).never
- assert_difference("Ship.count", -1) { @pirate.save! }
+ assert_not_called(@pirate.ship, :valid?) do
+ assert_difference("Ship.count", -1) { @pirate.save! }
+ end
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice
@@ -795,7 +796,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@ship.pirate.catchphrase = "Changed Catchphrase"
@ship.name_will_change!
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_not_nil @pirate.reload.ship
end
@@ -806,18 +807,19 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved
- @pirate.ship.expects(:save).never
- assert @pirate.save
+ assert_not_called(@pirate.ship, :save) do
+ assert @pirate.save
+ end
end
# belongs_to
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?
+ assert_not_predicate @ship.pirate, :marked_for_destruction?
@ship.pirate.mark_for_destruction
id = @ship.pirate.id
- assert @ship.pirate.marked_for_destruction?
+ assert_predicate @ship.pirate, :marked_for_destruction?
assert Pirate.find_by_id(id)
@ship.save
@@ -827,11 +829,12 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
def test_should_skip_validation_on_a_parent_association_if_marked_for_destruction
@ship.pirate.catchphrase = ""
- assert !@ship.valid?
+ assert_not_predicate @ship, :valid?
@ship.pirate.mark_for_destruction
- @ship.pirate.expects(:valid?).never
- assert_difference("Pirate.count", -1) { @ship.save! }
+ assert_not_called(@ship.pirate, :valid?) do
+ assert_difference("Pirate.count", -1) { @ship.save! }
+ end
end
def test_a_parent_marked_for_destruction_should_not_be_destroyed_twice
@@ -855,7 +858,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@ship.pirate.catchphrase = "Changed Catchphrase"
- assert_raise(RuntimeError) { assert !@ship.save }
+ assert_raise(RuntimeError) { assert_not @ship.save }
assert_not_nil @ship.reload.pirate
end
@@ -871,7 +874,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
def test_should_destroy_has_many_as_part_of_the_save_transaction_if_they_were_marked_for_destruction
2.times { |i| @pirate.birds.create!(name: "birds_#{i}") }
- assert !@pirate.birds.any?(&:marked_for_destruction?)
+ assert_not @pirate.birds.any?(&:marked_for_destruction?)
@pirate.birds.each(&:mark_for_destruction)
klass = @pirate.birds.first.class
@@ -881,7 +884,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
ids.each { |id| assert klass.find_by_id(id) }
@pirate.save
- assert @pirate.reload.birds.empty?
+ assert_empty @pirate.reload.birds
ids.each { |id| assert_nil klass.find_by_id(id) }
end
@@ -889,30 +892,32 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.birds.create!(name: :parrot)
@pirate.birds.first.destroy
@pirate.save!
- assert @pirate.reload.birds.empty?
+ assert_empty @pirate.reload.birds
end
def test_should_skip_validation_on_has_many_if_marked_for_destruction
2.times { |i| @pirate.birds.create!(name: "birds_#{i}") }
@pirate.birds.each { |bird| bird.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
- @pirate.birds.each do |bird|
- bird.mark_for_destruction
- bird.expects(:valid?).never
+ @pirate.birds.each(&:mark_for_destruction)
+
+ assert_not_called(@pirate.birds.first, :valid?) do
+ assert_not_called(@pirate.birds.last, :valid?) do
+ assert_difference("Bird.count", -2) { @pirate.save! }
+ end
end
- assert_difference("Bird.count", -2) { @pirate.save! }
end
def test_should_skip_validation_on_has_many_if_destroyed
@pirate.birds.create!(name: "birds_1")
@pirate.birds.each { |bird| bird.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
@pirate.birds.each(&:destroy)
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_has_many
@@ -921,8 +926,11 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.birds.each(&:mark_for_destruction)
assert @pirate.save
- @pirate.birds.each { |bird| bird.expects(:destroy).never }
- assert @pirate.save
+ @pirate.birds.each do |bird|
+ assert_not_called(bird, :destroy) do
+ assert @pirate.save
+ end
+ end
end
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_has_many
@@ -937,7 +945,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
end
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, @pirate.reload.birds
end
@@ -1003,42 +1011,44 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
def test_should_destroy_habtm_as_part_of_the_save_transaction_if_they_were_marked_for_destruction
2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") }
- assert !@pirate.parrots.any?(&:marked_for_destruction?)
+ assert_not @pirate.parrots.any?(&:marked_for_destruction?)
@pirate.parrots.each(&:mark_for_destruction)
assert_no_difference "Parrot.count" do
@pirate.save
end
- assert @pirate.reload.parrots.empty?
+ assert_empty @pirate.reload.parrots
join_records = Pirate.connection.select_all("SELECT * FROM parrots_pirates WHERE pirate_id = #{@pirate.id}")
- assert join_records.empty?
+ assert_empty join_records
end
def test_should_skip_validation_on_habtm_if_marked_for_destruction
2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") }
@pirate.parrots.each { |parrot| parrot.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
+
+ @pirate.parrots.each { |parrot| parrot.mark_for_destruction }
- @pirate.parrots.each do |parrot|
- parrot.mark_for_destruction
- parrot.expects(:valid?).never
+ assert_not_called(@pirate.parrots.first, :valid?) do
+ assert_not_called(@pirate.parrots.last, :valid?) do
+ @pirate.save!
+ end
end
- @pirate.save!
- assert @pirate.reload.parrots.empty?
+ assert_empty @pirate.reload.parrots
end
def test_should_skip_validation_on_habtm_if_destroyed
@pirate.parrots.create!(name: "parrots_1")
@pirate.parrots.each { |parrot| parrot.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
@pirate.parrots.each(&:destroy)
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_habtm
@@ -1065,7 +1075,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
end
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, @pirate.reload.parrots
end
@@ -1145,16 +1155,16 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_automatically_validate_the_associated_model
@pirate.ship.name = ""
- assert @pirate.invalid?
- assert @pirate.errors[:"ship.name"].any?
+ assert_predicate @pirate, :invalid?
+ assert_predicate @pirate.errors[:"ship.name"], :any?
end
def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
@pirate.ship.name = nil
@pirate.catchphrase = nil
- assert @pirate.invalid?
- assert @pirate.errors[:"ship.name"].any?
- assert @pirate.errors[:catchphrase].any?
+ assert_predicate @pirate, :invalid?
+ assert_predicate @pirate.errors[:"ship.name"], :any?
+ assert_predicate @pirate.errors[:catchphrase], :any?
end
def test_should_not_ignore_different_error_messages_on_the_same_attribute
@@ -1163,7 +1173,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
Ship.validates_format_of :name, with: /\w/
@pirate.ship.name = ""
@pirate.catchphrase = nil
- assert @pirate.invalid?
+ assert_predicate @pirate, :invalid?
assert_equal ["can't be blank", "is invalid"], @pirate.errors[:"ship.name"]
ensure
Ship._validators = old_validators if old_validators
@@ -1213,7 +1223,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
assert_no_difference "Pirate.count" do
assert_no_difference "Ship.count" do
- assert !pirate.save
+ assert_not pirate.save
end
end
end
@@ -1232,7 +1242,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
end
end
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, [@pirate.reload.catchphrase, @pirate.ship.name]
end
@@ -1244,7 +1254,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
ship = ShipWithoutNestedAttributes.new(name: "The Black Flag")
ship.parts.build.mark_for_destruction
- assert_not ship.valid?
+ assert_not_predicate ship, :valid?
end
end
@@ -1299,16 +1309,16 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_automatically_validate_the_associated_model
@ship.pirate.catchphrase = ""
- assert @ship.invalid?
- assert @ship.errors[:"pirate.catchphrase"].any?
+ assert_predicate @ship, :invalid?
+ assert_predicate @ship.errors[:"pirate.catchphrase"], :any?
end
def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid
@ship.name = nil
@ship.pirate.catchphrase = nil
- assert @ship.invalid?
- assert @ship.errors[:name].any?
- assert @ship.errors[:"pirate.catchphrase"].any?
+ assert_predicate @ship, :invalid?
+ assert_predicate @ship.errors[:name], :any?
+ assert_predicate @ship.errors[:"pirate.catchphrase"], :any?
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
@@ -1337,7 +1347,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
assert_no_difference "Ship.count" do
assert_no_difference "Pirate.count" do
- assert !ship.save
+ assert_not ship.save
end
end
end
@@ -1356,7 +1366,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
end
end
- assert_raise(RuntimeError) { assert !@ship.save }
+ assert_raise(RuntimeError) { assert_not @ship.save }
assert_equal before, [@ship.pirate.reload.catchphrase, @ship.reload.name]
end
@@ -1403,17 +1413,17 @@ module AutosaveAssociationOnACollectionAssociationTests
def test_should_automatically_validate_the_associated_models
@pirate.send(@association_name).each { |child| child.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
- assert @pirate.errors[@association_name].empty?
+ assert_empty @pirate.errors[@association_name]
end
def test_should_not_use_default_invalid_error_on_associated_models
@pirate.send(@association_name).build(name: "")
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
- assert @pirate.errors[@association_name].empty?
+ assert_empty @pirate.errors[@association_name]
end
def test_should_default_invalid_error_from_i18n
@@ -1423,10 +1433,10 @@ module AutosaveAssociationOnACollectionAssociationTests
@pirate.send(@association_name).build(name: "")
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
assert_equal ["cannot be blank"], @pirate.errors["#{@association_name}.name"]
assert_equal ["#{@association_name.to_s.humanize} name cannot be blank"], @pirate.errors.full_messages
- assert @pirate.errors[@association_name].empty?
+ assert_empty @pirate.errors[@association_name]
ensure
I18n.backend = I18n::Backend::Simple.new
end
@@ -1435,9 +1445,9 @@ module AutosaveAssociationOnACollectionAssociationTests
@pirate.send(@association_name).each { |child| child.name = "" }
@pirate.catchphrase = nil
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
- assert @pirate.errors[:catchphrase].any?
+ assert_predicate @pirate.errors[:catchphrase], :any?
end
def test_should_allow_to_bypass_validations_on_the_associated_models_on_update
@@ -1480,7 +1490,7 @@ module AutosaveAssociationOnACollectionAssociationTests
@child_1.name = "Changed"
@child_1.cancel_save_from_callback = true
- assert !@pirate.save
+ assert_not @pirate.save
assert_equal "Don' botharrr talkin' like one, savvy?", @pirate.reload.catchphrase
assert_equal "Posideons Killer", @child_1.reload.name
@@ -1490,7 +1500,7 @@ module AutosaveAssociationOnACollectionAssociationTests
assert_no_difference "Pirate.count" do
assert_no_difference "#{new_child.class.name}.count" do
- assert !new_pirate.save
+ assert_not new_pirate.save
end
end
end
@@ -1510,7 +1520,7 @@ module AutosaveAssociationOnACollectionAssociationTests
end
end
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, [@pirate.reload.catchphrase, *@pirate.send(@association_name).map(&:name)]
end
@@ -1595,10 +1605,10 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te
end
test "should automatically validate associations" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.birds.each { |bird| bird.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
end
end
@@ -1613,15 +1623,15 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes
end
test "should automatically validate associations with :validate => true" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.ship.name = ""
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
end
test "should not automatically add validate associations without :validate => true" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.non_validated_ship.name = ""
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
end
end
@@ -1634,15 +1644,15 @@ class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord::
end
test "should automatically validate associations with :validate => true" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.parrot = Parrot.new(name: "")
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
end
test "should not automatically validate associations without :validate => true" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.non_validated_parrot = Parrot.new(name: "")
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
end
end
@@ -1655,17 +1665,17 @@ class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::Test
end
test "should automatically validate associations with :validate => true" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.parrots = [ Parrot.new(name: "popuga") ]
@pirate.parrots.each { |parrot| parrot.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
end
test "should not automatically validate associations without :validate => true" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.non_validated_parrots = [ Parrot.new(name: "popuga") ]
@pirate.non_validated_parrots.each { |parrot| parrot.name = "" }
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
end
end
@@ -1686,7 +1696,7 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas
end
test "should not generate validation methods for has_one associations without :validate => true" do
- assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_ship)
+ assert_not_respond_to @pirate, :validate_associated_records_for_non_validated_ship
end
test "should generate validation methods for belongs_to associations with :validate => true" do
@@ -1694,7 +1704,7 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas
end
test "should not generate validation methods for belongs_to associations without :validate => true" do
- assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrot)
+ assert_not_respond_to @pirate, :validate_associated_records_for_non_validated_parrot
end
test "should generate validation methods for HABTM associations with :validate => true" do
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 1a1d4ce039..fd008ca8e3 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -104,7 +104,7 @@ class BasicsTest < ActiveRecord::TestCase
pk = Author.columns_hash["id"]
ref = Post.columns_hash["author_id"]
- assert_equal pk.bigint?, ref.bigint?
+ assert_equal pk.sql_type, ref.sql_type
end
def test_many_mutations
@@ -147,8 +147,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_table_exists
- assert !NonExistentTable.table_exists?
- assert Topic.table_exists?
+ assert_not_predicate NonExistentTable, :table_exists?
+ assert_predicate Topic, :table_exists?
end
def test_preserving_date_objects
@@ -307,7 +307,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal "Dude", cbs[0].name
assert_equal "Bob", cbs[1].name
assert cbs[0].frickinawesome
- assert !cbs[1].frickinawesome
+ assert_not cbs[1].frickinawesome
end
def test_load
@@ -450,7 +450,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_default_values
topic = Topic.new
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_nil topic.written_on
assert_nil topic.bonus_time
assert_nil topic.last_read
@@ -458,7 +458,7 @@ class BasicsTest < ActiveRecord::TestCase
topic.save
topic = Topic.find(topic.id)
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_nil topic.last_read
# Oracle has some funky default handling, so it requires a bit of
@@ -629,7 +629,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_readonly_attributes
- assert_equal Set.new([ "title" , "comments_count" ]), ReadonlyTitlePost.readonly_attributes
+ assert_equal Set.new([ "title", "comments_count" ]), ReadonlyTitlePost.readonly_attributes
post = ReadonlyTitlePost.create(title: "cannot change this", body: "changeable")
post.reload
@@ -727,9 +727,9 @@ class BasicsTest < ActiveRecord::TestCase
b_nil = Boolean.find(nil_id)
assert_nil b_nil.value
b_false = Boolean.find(false_id)
- assert !b_false.value?
+ assert_not_predicate b_false, :value?
b_true = Boolean.find(true_id)
- assert b_true.value?
+ assert_predicate b_true, :value?
end
def test_boolean_without_questionmark
@@ -753,9 +753,9 @@ class BasicsTest < ActiveRecord::TestCase
b_blank = Boolean.find(blank_id)
assert_nil b_blank.value
b_false = Boolean.find(false_id)
- assert !b_false.value?
+ assert_not_predicate b_false, :value?
b_true = Boolean.find(true_id)
- assert b_true.value?
+ assert_predicate b_true, :value?
end
def test_new_record_returns_boolean
@@ -768,7 +768,7 @@ class BasicsTest < ActiveRecord::TestCase
duped_topic = nil
assert_nothing_raised { duped_topic = topic.dup }
assert_equal topic.title, duped_topic.title
- assert !duped_topic.persisted?
+ assert_not_predicate duped_topic, :persisted?
# test if the attributes have been duped
topic.title = "a"
@@ -786,7 +786,7 @@ class BasicsTest < ActiveRecord::TestCase
# test if saved clone object differs from original
duped_topic.save
- assert duped_topic.persisted?
+ assert_predicate duped_topic, :persisted?
assert_not_equal duped_topic.id, topic.id
duped_topic.reload
@@ -807,7 +807,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_nothing_raised { dup = dev.dup }
assert_kind_of DeveloperSalary, dup.salary
assert_equal dev.salary.amount, dup.salary.amount
- assert !dup.persisted?
+ assert_not_predicate dup, :persisted?
# test if the attributes have been duped
original_amount = dup.salary.amount
@@ -815,7 +815,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal original_amount, dup.salary.amount
assert dup.save
- assert dup.persisted?
+ assert_predicate dup, :persisted?
assert_not_equal dup.id, dev.id
end
@@ -835,52 +835,52 @@ class BasicsTest < ActiveRecord::TestCase
def test_clone_of_new_object_with_defaults
developer = Developer.new
- assert !developer.name_changed?
- assert !developer.salary_changed?
+ assert_not_predicate developer, :name_changed?
+ assert_not_predicate developer, :salary_changed?
cloned_developer = developer.clone
- assert !cloned_developer.name_changed?
- assert !cloned_developer.salary_changed?
+ assert_not_predicate cloned_developer, :name_changed?
+ assert_not_predicate cloned_developer, :salary_changed?
end
def test_clone_of_new_object_marks_attributes_as_dirty
developer = Developer.new name: "Bjorn", salary: 100000
- assert developer.name_changed?
- assert developer.salary_changed?
+ assert_predicate developer, :name_changed?
+ assert_predicate developer, :salary_changed?
cloned_developer = developer.clone
- assert cloned_developer.name_changed?
- assert cloned_developer.salary_changed?
+ assert_predicate cloned_developer, :name_changed?
+ assert_predicate cloned_developer, :salary_changed?
end
def test_clone_of_new_object_marks_as_dirty_only_changed_attributes
developer = Developer.new name: "Bjorn"
assert developer.name_changed? # obviously
- assert !developer.salary_changed? # attribute has non-nil default value, so treated as not changed
+ assert_not developer.salary_changed? # attribute has non-nil default value, so treated as not changed
cloned_developer = developer.clone
- assert cloned_developer.name_changed?
- assert !cloned_developer.salary_changed? # ... and cloned instance should behave same
+ assert_predicate cloned_developer, :name_changed?
+ assert_not cloned_developer.salary_changed? # ... and cloned instance should behave same
end
def test_dup_of_saved_object_marks_attributes_as_dirty
developer = Developer.create! name: "Bjorn", salary: 100000
- assert !developer.name_changed?
- assert !developer.salary_changed?
+ assert_not_predicate developer, :name_changed?
+ assert_not_predicate developer, :salary_changed?
cloned_developer = developer.dup
assert cloned_developer.name_changed? # both attributes differ from defaults
- assert cloned_developer.salary_changed?
+ assert_predicate cloned_developer, :salary_changed?
end
def test_dup_of_saved_object_marks_as_dirty_only_changed_attributes
developer = Developer.create! name: "Bjorn"
- assert !developer.name_changed? # both attributes of saved object should be treated as not changed
- assert !developer.salary_changed?
+ assert_not developer.name_changed? # both attributes of saved object should be treated as not changed
+ assert_not_predicate developer, :salary_changed?
cloned_developer = developer.dup
assert cloned_developer.name_changed? # ... but on cloned object should be
- assert !cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be treated as not changed on cloned instance
+ assert_not cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be treated as not changed on cloned instance
end
def test_bignum
@@ -891,11 +891,9 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 2147483648, company.rating
end
- unless current_adapter?(:SQLite3Adapter)
- def test_bignum_pk
- company = Company.create!(id: 2147483648, name: "foo")
- assert_equal company, Company.find(company.id)
- end
+ def test_bignum_pk
+ company = Company.create!(id: 2147483648, name: "foo")
+ assert_equal company, Company.find(company.id)
end
# TODO: extend defaults tests to other databases!
@@ -953,14 +951,14 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_toggle_attribute
- assert !topics(:first).approved?
+ assert_not_predicate topics(:first), :approved?
topics(:first).toggle!(:approved)
- assert topics(:first).approved?
+ assert_predicate topics(:first), :approved?
topic = topics(:first)
topic.toggle(:approved)
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
topic.reload
- assert topic.approved?
+ assert_predicate topic, :approved?
end
def test_reload
@@ -1433,28 +1431,92 @@ class BasicsTest < ActiveRecord::TestCase
test "resetting column information doesn't remove attribute methods" do
topic = topics(:first)
- assert_not topic.id_changed?
+ assert_not_predicate topic, :id_changed?
Topic.reset_column_information
- assert_not topic.id_changed?
+ assert_not_predicate topic, :id_changed?
end
test "ignored columns are not present in columns_hash" do
cache_columns = Developer.connection.schema_cache.columns_hash(Developer.table_name)
assert_includes cache_columns.keys, "first_name"
assert_not_includes Developer.columns_hash.keys, "first_name"
+ assert_not_includes SubDeveloper.columns_hash.keys, "first_name"
+ assert_not_includes SymbolIgnoredDeveloper.columns_hash.keys, "first_name"
end
test "ignored columns have no attribute methods" do
- refute Developer.new.respond_to?(:first_name)
- refute Developer.new.respond_to?(:first_name=)
- refute Developer.new.respond_to?(:first_name?)
+ assert_not_respond_to Developer.new, :first_name
+ assert_not_respond_to Developer.new, :first_name=
+ assert_not_respond_to Developer.new, :first_name?
+ assert_not_respond_to SubDeveloper.new, :first_name
+ assert_not_respond_to SubDeveloper.new, :first_name=
+ assert_not_respond_to SubDeveloper.new, :first_name?
+ assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name
+ assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name=
+ assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name?
end
test "ignored columns don't prevent explicit declaration of attribute methods" do
- assert Developer.new.respond_to?(:last_name)
- assert Developer.new.respond_to?(:last_name=)
- assert Developer.new.respond_to?(:last_name?)
+ assert_respond_to Developer.new, :last_name
+ assert_respond_to Developer.new, :last_name=
+ assert_respond_to Developer.new, :last_name?
+ assert_respond_to SubDeveloper.new, :last_name
+ assert_respond_to SubDeveloper.new, :last_name=
+ assert_respond_to SubDeveloper.new, :last_name?
+ assert_respond_to SymbolIgnoredDeveloper.new, :last_name
+ assert_respond_to SymbolIgnoredDeveloper.new, :last_name=
+ assert_respond_to SymbolIgnoredDeveloper.new, :last_name?
+ end
+
+ test "ignored columns are stored as an array of string" do
+ assert_equal(%w(first_name last_name), Developer.ignored_columns)
+ assert_equal(%w(first_name last_name), SymbolIgnoredDeveloper.ignored_columns)
+ end
+
+ test "when #reload called, ignored columns' attribute methods are not defined" do
+ developer = Developer.create!(name: "Developer")
+ assert_not_respond_to developer, :first_name
+ assert_not_respond_to developer, :first_name=
+
+ developer.reload
+
+ assert_not_respond_to developer, :first_name
+ assert_not_respond_to developer, :first_name=
+ end
+
+ test "ignored columns not included in SELECT" do
+ query = Developer.all.to_sql.downcase
+
+ # ignored column
+ assert_not query.include?("first_name")
+
+ # regular column
+ assert query.include?("name")
+ end
+
+ test "column names are quoted when using #from clause and model has ignored columns" do
+ assert_not_empty Developer.ignored_columns
+ query = Developer.from("developers").to_sql
+ quoted_id = "#{Developer.quoted_table_name}.#{Developer.quoted_primary_key}"
+
+ assert_match(/SELECT #{Regexp.escape(quoted_id)}.* FROM developers/, query)
+ end
+
+ test "using table name qualified column names unless having SELECT list explicitly" do
+ assert_equal developers(:david), Developer.from("developers").joins(:shared_computers).take
+ end
+
+ test "protected environments by default is an array with production" do
+ assert_equal ["production"], ActiveRecord::Base.protected_environments
+ end
+
+ def test_protected_environments_are_stored_as_an_array_of_string
+ previous_protected_environments = ActiveRecord::Base.protected_environments
+ ActiveRecord::Base.protected_environments = [:staging, "production"]
+ assert_equal ["staging", "production"], ActiveRecord::Base.protected_environments
+ ensure
+ ActiveRecord::Base.protected_environments = previous_protected_environments
end
end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index c965404b07..c8163901c6 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -313,7 +313,7 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_each_record_should_yield_record_if_block_is_given
assert_queries(6) do
Post.in_batches(of: 2).each_record do |post|
- assert post.title.present?
+ assert_predicate post.title, :present?
assert_kind_of Post, post
end
end
@@ -322,7 +322,7 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_each_record_should_return_enumerator_if_no_block_given
assert_queries(6) do
Post.in_batches(of: 2).each_record.with_index do |post, i|
- assert post.title.present?
+ assert_predicate post.title, :present?
assert_kind_of Post, post
end
end
@@ -353,24 +353,24 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_should_not_be_loaded
Post.in_batches(of: 1) do |relation|
- assert_not relation.loaded?
+ assert_not_predicate relation, :loaded?
end
Post.in_batches(of: 1, load: false) do |relation|
- assert_not relation.loaded?
+ assert_not_predicate relation, :loaded?
end
end
def test_in_batches_should_be_loaded
Post.in_batches(of: 1, load: true) do |relation|
- assert relation.loaded?
+ assert_predicate relation, :loaded?
end
end
def test_in_batches_if_not_loaded_executes_more_queries
assert_queries(@total + 1) do
Post.in_batches(of: 1, load: false) do |relation|
- assert_not relation.loaded?
+ assert_not_predicate relation, :loaded?
end
end
end
@@ -508,7 +508,7 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches
Post.update_all(author_id: 0)
person = Post.last
- person.update_attributes(author_id: 1)
+ person.update(author_id: 1)
Post.in_batches(of: 2) do |batch|
batch.where("author_id >= 1").update_all("author_id = author_id + 1")
@@ -586,41 +586,17 @@ class EachTest < ActiveRecord::TestCase
end
end
- test ".error_on_ignored_order_or_limit= is deprecated" do
- begin
- prev = ActiveRecord::Base.error_on_ignored_order
- assert_deprecated "Please use error_on_ignored_order= instead." do
- ActiveRecord::Base.error_on_ignored_order_or_limit = true
- end
- assert ActiveRecord::Base.error_on_ignored_order
- ensure
- ActiveRecord::Base.error_on_ignored_order = prev
- end
- end
-
- test ".error_on_ignored_order_or_limit is deprecated" do
- expected = ActiveRecord::Base.error_on_ignored_order
- actual = assert_deprecated "Please use error_on_ignored_order instead." do
- ActiveRecord::Base.error_on_ignored_order_or_limit
- end
- assert_equal expected, actual
- end
-
- test "#error_on_ignored_order_or_limit is deprecated" do
- expected = ActiveRecord::Base.error_on_ignored_order
- actual = assert_deprecated "Please use error_on_ignored_order instead." do
- Post.new.error_on_ignored_order_or_limit
- end
- assert_equal expected, actual
- end
-
test ".find_each respects table alias" do
assert_queries(1) do
table_alias = Post.arel_table.alias("omg_posts")
table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias)
predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
- posts = ActiveRecord::Relation.create(Post, table_alias, predicate_builder)
+ posts = ActiveRecord::Relation.create(
+ Post,
+ table: table_alias,
+ predicate_builder: predicate_builder
+ )
posts.find_each {}
end
end
diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb
index 8f2f2c6186..3a569f226e 100644
--- a/activerecord/test/cases/cache_key_test.rb
+++ b/activerecord/test/cases/cache_key_test.rb
@@ -38,8 +38,8 @@ module ActiveRecord
end
test "cache_version is only there when versioning is on" do
- assert CacheMeWithVersion.create.cache_version.present?
- assert_not CacheMe.create.cache_version.present?
+ assert_predicate CacheMeWithVersion.create.cache_version, :present?
+ assert_not_predicate CacheMe.create.cache_version, :present?
end
test "cache_key_with_version always has both key and version" do
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index b47fd0af41..080d2a54bc 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -21,7 +21,7 @@ require "models/comment"
require "models/rating"
class CalculationsTest < ActiveRecord::TestCase
- fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books
+ fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books, :posts, :comments
def test_should_sum_field
assert_equal 318, Account.sum(:credit_limit)
@@ -236,6 +236,24 @@ class CalculationsTest < ActiveRecord::TestCase
end
end
+ def test_count_with_eager_loading_and_custom_order
+ posts = Post.includes(:comments).order("comments.id")
+ assert_queries(1) { assert_equal 11, posts.count }
+ assert_queries(1) { assert_equal 11, posts.count(:all) }
+ end
+
+ def test_count_with_eager_loading_and_custom_order_and_distinct
+ posts = Post.includes(:comments).order("comments.id").distinct
+ assert_queries(1) { assert_equal 11, posts.count }
+ assert_queries(1) { assert_equal 11, posts.count(:all) }
+ end
+
+ def test_distinct_count_all_with_custom_select_and_order
+ accounts = Account.distinct.select("credit_limit % 10").order(Arel.sql("credit_limit % 10"))
+ assert_queries(1) { assert_equal 3, accounts.count(:all) }
+ assert_queries(1) { assert_equal 3, accounts.load.size }
+ end
+
def test_distinct_count_with_order_and_limit
assert_equal 4, Account.distinct.order(:firm_id).limit(4).count
end
@@ -260,6 +278,10 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).limit(3).offset(2).count
end
+ def test_distinct_count_with_group_by_and_order_and_limit
+ assert_equal({ 6 => 2 }, Account.group(:firm_id).distinct.order("1 DESC").limit(1).count)
+ end
+
def test_should_group_by_summed_field_having_condition
c = Account.group(:firm_id).having("sum(credit_limit) > 50").sum(:credit_limit)
assert_nil c[1]
@@ -659,14 +681,14 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_pluck_with_selection_clause
- assert_equal [50, 53, 55, 60], Account.pluck("DISTINCT credit_limit").sort
- assert_equal [50, 53, 55, 60], Account.pluck("DISTINCT accounts.credit_limit").sort
- assert_equal [50, 53, 55, 60], Account.pluck("DISTINCT(credit_limit)").sort
+ assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT credit_limit")).sort
+ assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT accounts.credit_limit")).sort
+ assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT(credit_limit)")).sort
# MySQL returns "SUM(DISTINCT(credit_limit))" as the column name unless
# an alias is provided. Without the alias, the column cannot be found
# and properly typecast.
- assert_equal [50 + 53 + 55 + 60], Account.pluck("SUM(DISTINCT(credit_limit)) as credit_limit")
+ assert_equal [50 + 53 + 55 + 60], Account.pluck(Arel.sql("SUM(DISTINCT(credit_limit)) as credit_limit"))
end
def test_plucks_with_ids
@@ -678,6 +700,11 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal [], Topic.includes(:replies).limit(1).where("0 = 1").pluck(:id)
end
+ def test_pluck_with_includes_offset
+ assert_equal [5], Topic.includes(:replies).order(:id).offset(4).pluck(:id)
+ assert_equal [], Topic.includes(:replies).order(:id).offset(5).pluck(:id)
+ end
+
def test_pluck_not_auto_table_name_prefix_if_column_included
Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
ids = Company.includes(:contracts).pluck(:developer_id)
@@ -768,10 +795,27 @@ class CalculationsTest < ActiveRecord::TestCase
companies = Company.order(:name).limit(3).load
assert_queries 1 do
- assert_equal ["37signals", "Apex", "Ex Nihilo"], companies.pluck("DISTINCT name")
+ assert_equal ["37signals", "Apex", "Ex Nihilo"], companies.pluck(Arel.sql("DISTINCT name"))
end
end
+ def test_pick_one
+ assert_equal "The First Topic", Topic.order(:id).pick(:heading)
+ assert_nil Topic.none.pick(:heading)
+ assert_nil Topic.where("1=0").pick(:heading)
+ end
+
+ def test_pick_two
+ assert_equal ["David", "david@loudthinking.com"], Topic.order(:id).pick(:author_name, :author_email_address)
+ assert_nil Topic.none.pick(:author_name, :author_email_address)
+ assert_nil Topic.where("1=0").pick(:author_name, :author_email_address)
+ end
+
+ def test_pick_delegate_to_all
+ cool_first = minivans(:cool_first)
+ assert_equal cool_first.color, Minivan.pick(:color)
+ end
+
def test_grouped_calculation_with_polymorphic_relation
part = ShipPart.create!(name: "has trinket")
part.trinkets.create!
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index 55c7475f46..b9ba51c730 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -385,60 +385,60 @@ class CallbacksTest < ActiveRecord::TestCase
end
def assert_save_callbacks_not_called(someone)
- assert !someone.after_save_called
- assert !someone.after_create_called
- assert !someone.after_update_called
+ assert_not someone.after_save_called
+ assert_not someone.after_create_called
+ assert_not someone.after_update_called
end
private :assert_save_callbacks_not_called
def test_before_create_throwing_abort
someone = CallbackHaltedDeveloper.new
someone.cancel_before_create = true
- assert someone.valid?
- assert !someone.save
+ assert_predicate someone, :valid?
+ assert_not someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_save_throwing_abort
david = DeveloperWithCanceledCallbacks.find(1)
- assert david.valid?
- assert !david.save
+ assert_predicate david, :valid?
+ assert_not david.save
exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
assert_equal david, exc.record
david = DeveloperWithCanceledCallbacks.find(1)
david.salary = 10_000_000
- assert !david.valid?
- assert !david.save
+ assert_not_predicate david, :valid?
+ assert_not david.save
assert_raise(ActiveRecord::RecordInvalid) { david.save! }
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_save = true
- assert someone.valid?
- assert !someone.save
+ assert_predicate someone, :valid?
+ assert_not someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_update_throwing_abort
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_update = true
- assert someone.valid?
- assert !someone.save
+ assert_predicate someone, :valid?
+ assert_not someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_destroy_throwing_abort
david = DeveloperWithCanceledCallbacks.find(1)
- assert !david.destroy
+ assert_not david.destroy
exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! }
assert_equal david, exc.record
assert_not_nil ImmutableDeveloper.find_by_id(1)
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_destroy = true
- assert !someone.destroy
+ assert_not someone.destroy
assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! }
- assert !someone.after_destroy_called
+ assert_not someone.after_destroy_called
end
def test_callback_throwing_abort
@@ -467,12 +467,12 @@ class CallbacksTest < ActiveRecord::TestCase
def test_inheritance_of_callbacks
parent = ParentDeveloper.new
- assert !parent.after_save_called
+ assert_not parent.after_save_called
parent.save
assert parent.after_save_called
child = ChildDeveloper.new
- assert !child.after_save_called
+ assert_not child.after_save_called
child.save
assert child.after_save_called
end
diff --git a/activerecord/test/cases/clone_test.rb b/activerecord/test/cases/clone_test.rb
index 3187e6aed5..65e5016040 100644
--- a/activerecord/test/cases/clone_test.rb
+++ b/activerecord/test/cases/clone_test.rb
@@ -36,7 +36,7 @@ module ActiveRecord
cloned = Topic.new
clone = cloned.clone
cloned.freeze
- assert_not clone.frozen?
+ assert_not_predicate clone, :frozen?
end
end
end
diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb
index c137693211..a5d908344a 100644
--- a/activerecord/test/cases/collection_cache_key_test.rb
+++ b/activerecord/test/cases/collection_cache_key_test.rb
@@ -24,7 +24,7 @@ module ActiveRecord
/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key
- assert_equal Digest::MD5.hexdigest(developers.to_sql), $1
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
assert_equal developers.count.to_s, $2
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
end
@@ -37,7 +37,7 @@ module ActiveRecord
/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key
- assert_equal Digest::MD5.hexdigest(developers.to_sql), $1
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
assert_equal developers.count.to_s, $2
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
end
@@ -50,7 +50,7 @@ module ActiveRecord
/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key
- assert_equal Digest::MD5.hexdigest(developers.to_sql), $1
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
assert_equal developers.count.to_s, $2
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
end
@@ -60,7 +60,11 @@ module ActiveRecord
table_metadata = ActiveRecord::TableMetadata.new(Developer, table_alias)
predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
- developers = ActiveRecord::Relation.create(Developer, table_alias, predicate_builder)
+ developers = ActiveRecord::Relation.create(
+ Developer,
+ table: table_alias,
+ predicate_builder: predicate_builder
+ )
developers = developers.where(salary: 100000).order(updated_at: :desc)
last_developer_timestamp = developers.first.updated_at
@@ -68,11 +72,21 @@ module ActiveRecord
/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key
- assert_equal Digest::MD5.hexdigest(developers.to_sql), $1
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
assert_equal developers.count.to_s, $2
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
end
+ test "cache_key for relation with includes" do
+ comments = Comment.includes(:post).where("posts.type": "Post")
+ assert_match(/\Acomments\/query-(\h+)-(\d+)-(\d+)\z/, comments.cache_key)
+ end
+
+ test "cache_key for loaded relation with includes" do
+ comments = Comment.includes(:post).where("posts.type": "Post").load
+ assert_match(/\Acomments\/query-(\h+)-(\d+)-(\d+)\z/, comments.cache_key)
+ end
+
test "it triggers at most one query" do
developers = Developer.where(name: "David")
@@ -131,5 +145,17 @@ module ActiveRecord
assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
end
+
+ test "cache_key with a relation having distinct and order" do
+ developers = Developer.distinct.order(:salary).limit(5)
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
+ end
+
+ test "cache_key with a relation having custom select and order" do
+ developers = Developer.select("name AS dev_name").order("dev_name DESC").limit(5)
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
+ end
end
end
diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb
index 1bcafd4b55..584e03d196 100644
--- a/activerecord/test/cases/comment_test.rb
+++ b/activerecord/test/cases/comment_test.rb
@@ -142,5 +142,27 @@ if ActiveRecord::Base.connection.supports_comments?
assert_match %r[t\.string\s+"absent_comment"\n], output
assert_no_match %r[t\.string\s+"absent_comment", comment:\n], output
end
+
+ def test_change_table_comment
+ @connection.change_table_comment :commenteds, "Edited table comment"
+ assert_equal "Edited table comment", @connection.table_comment("commenteds")
+ end
+
+ def test_change_table_comment_to_nil
+ @connection.change_table_comment :commenteds, nil
+ assert_nil @connection.table_comment("commenteds")
+ end
+
+ def test_change_column_comment
+ @connection.change_column_comment :commenteds, :name, "Edited column comment"
+ column = Commented.columns_hash["name"]
+ assert_equal "Edited column comment", column.comment
+ end
+
+ def test_change_column_comment_to_nil
+ @connection.change_column_comment :commenteds, :name, nil
+ column = Commented.columns_hash["name"]
+ assert_nil column.comment
+ end
end
end
diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
index 82c6cf8dea..72838ff56b 100644
--- a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
+++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
@@ -45,11 +45,11 @@ module ActiveRecord
# Make sure the pool marks the connection in use
assert_equal @adapter, pool.connection
- assert @adapter.in_use?
+ assert_predicate @adapter, :in_use?
# Close should put the adapter back in the pool
@adapter.close
- assert_not @adapter.in_use?
+ assert_not_predicate @adapter, :in_use?
assert_equal @adapter, pool.connection
end
diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
index 74d0ed348e..b8e623f17b 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -1,10 +1,15 @@
# frozen_string_literal: true
require "cases/helper"
+require "models/person"
module ActiveRecord
module ConnectionAdapters
class ConnectionHandlerTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ fixtures :people
+
def setup
@handler = ConnectionHandler.new
@spec_name = "primary"
@@ -66,6 +71,56 @@ module ActiveRecord
ENV["RAILS_ENV"] = previous_env
end
+ unless in_memory_db?
+ def test_establish_connection_using_3_level_config_defaults_to_default_env_primary_db
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+
+ config = {
+ "default_env" => {
+ "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" },
+ "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }
+ },
+ "another_env" => {
+ "primary" => { "adapter" => "sqlite3", "database" => "db/another-primary.sqlite3" },
+ "readonly" => { "adapter" => "sqlite3", "database" => "db/another-readonly.sqlite3" }
+ }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.establish_connection
+
+ assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ENV["RAILS_ENV"] = previous_env
+ ActiveRecord::Base.establish_connection(:arunit)
+ FileUtils.rm_rf "db"
+ end
+
+ def test_establish_connection_using_2_level_config_defaults_to_default_env_primary_db
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+
+ config = {
+ "default_env" => {
+ "adapter" => "sqlite3", "database" => "db/primary.sqlite3"
+ },
+ "another_env" => {
+ "adapter" => "sqlite3", "database" => "db/bad-primary.sqlite3"
+ }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.establish_connection
+
+ assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ENV["RAILS_ENV"] = previous_env
+ ActiveRecord::Base.establish_connection(:arunit)
+ FileUtils.rm_rf "db"
+ end
+ end
+
def test_establish_connection_using_two_level_configurations
config = { "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } }
@prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
@@ -98,11 +153,11 @@ module ActiveRecord
end
def test_active_connections?
- assert !@handler.active_connections?
+ assert_not_predicate @handler, :active_connections?
assert @handler.retrieve_connection(@spec_name)
- assert @handler.active_connections?
+ assert_predicate @handler, :active_connections?
@handler.clear_active_connections!
- assert !@handler.active_connections?
+ assert_not_predicate @handler, :active_connections?
end
def test_retrieve_connection_pool
@@ -139,6 +194,75 @@ module ActiveRecord
rd.close
end
+ def test_forked_child_doesnt_mangle_parent_connection
+ object_id = ActiveRecord::Base.connection.object_id
+ assert_predicate ActiveRecord::Base.connection, :active?
+
+ rd, wr = IO.pipe
+ rd.binmode
+ wr.binmode
+
+ pid = fork {
+ rd.close
+ if ActiveRecord::Base.connection.active?
+ wr.write Marshal.dump ActiveRecord::Base.connection.object_id
+ end
+ wr.close
+
+ exit # allow finalizers to run
+ }
+
+ wr.close
+
+ Process.waitpid pid
+ assert_not_equal object_id, Marshal.load(rd.read)
+ rd.close
+
+ assert_equal 3, ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM people")
+ end
+
+ unless in_memory_db?
+ def test_forked_child_recovers_from_disconnected_parent
+ object_id = ActiveRecord::Base.connection.object_id
+ assert_predicate ActiveRecord::Base.connection, :active?
+
+ rd, wr = IO.pipe
+ rd.binmode
+ wr.binmode
+
+ outer_pid = fork {
+ ActiveRecord::Base.connection.disconnect!
+
+ pid = fork {
+ rd.close
+ if ActiveRecord::Base.connection.active?
+ pair = [ActiveRecord::Base.connection.object_id,
+ ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM people")]
+ wr.write Marshal.dump pair
+ end
+ wr.close
+
+ exit # allow finalizers to run
+ }
+
+ Process.waitpid pid
+ }
+
+ wr.close
+
+ Process.waitpid outer_pid
+ child_id, child_count = Marshal.load(rd.read)
+
+ assert_not_equal object_id, child_id
+ rd.close
+
+ assert_equal 3, child_count
+
+ # Outer connection is unaffected
+ assert_equal 6, ActiveRecord::Base.connection.select_value("SELECT 2 * COUNT(*) FROM people")
+ end
+ end
+
def test_retrieve_connection_pool_copies_schema_cache_from_ancestor_pool
@pool.schema_cache = @pool.connection.schema_cache
@pool.schema_cache.add("posts")
@@ -200,15 +324,15 @@ module ActiveRecord
def test_a_class_using_custom_pool_and_switching_back_to_primary
klass2 = Class.new(Base) { def self.name; "klass2"; end }
- assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id
+ assert_same klass2.connection, ActiveRecord::Base.connection
pool = klass2.establish_connection(ActiveRecord::Base.connection_pool.spec.config)
- assert_equal klass2.connection.object_id, pool.connection.object_id
- refute_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id
+ assert_same klass2.connection, pool.connection
+ assert_not_same klass2.connection, ActiveRecord::Base.connection
klass2.remove_connection
- assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id
+ assert_same klass2.connection, ActiveRecord::Base.connection
end
def test_connection_specification_name_should_fallback_to_parent
@@ -223,8 +347,8 @@ module ActiveRecord
def test_remove_connection_should_not_remove_parent
klass2 = Class.new(Base) { def self.name; "klass2"; end }
klass2.remove_connection
- refute_nil ActiveRecord::Base.connection.object_id
- assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id
+ assert_not_nil ActiveRecord::Base.connection
+ assert_same klass2.connection, ActiveRecord::Base.connection
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index 006be9e65d..67496381d1 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -22,8 +22,8 @@ module ActiveRecord
new_cache = YAML.load(YAML.dump(@cache))
assert_no_queries do
- assert_equal 11, new_cache.columns("posts").size
- assert_equal 11, new_cache.columns_hash("posts").size
+ assert_equal 12, new_cache.columns("posts").size
+ assert_equal 12, new_cache.columns_hash("posts").size
assert new_cache.data_sources("posts")
assert_equal "id", new_cache.primary_keys("posts")
end
@@ -75,8 +75,8 @@ module ActiveRecord
@cache = Marshal.load(Marshal.dump(@cache))
assert_no_queries do
- assert_equal 11, @cache.columns("posts").size
- assert_equal 11, @cache.columns_hash("posts").size
+ assert_equal 12, @cache.columns("posts").size
+ assert_equal 12, @cache.columns_hash("posts").size
assert @cache.data_sources("posts")
assert_equal "id", @cache.primary_keys("posts")
end
diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
index 917a04ebc3..1c79d776f0 100644
--- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb
+++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
@@ -82,11 +82,11 @@ unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strin
end
def test_bigint_limit
- cast_type = @connection.send(:type_map).lookup("bigint")
+ limit = @connection.send(:type_map).lookup("bigint").send(:_limit)
if current_adapter?(:OracleAdapter)
- assert_equal 19, cast_type.limit
+ assert_equal 19, limit
else
- assert_equal 8, cast_type.limit
+ assert_equal 8, limit
end
end
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index 9d6ecbde78..0941ee3309 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -27,7 +27,7 @@ module ActiveRecord
# make sure we have an active connection
assert ActiveRecord::Base.connection
- assert ActiveRecord::Base.connection_handler.active_connections?
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
def test_app_delegation
@@ -47,14 +47,14 @@ module ActiveRecord
def test_connections_are_cleared_after_body_close
_, _, body = @management.call(@env)
body.close
- assert !ActiveRecord::Base.connection_handler.active_connections?
+ assert_not_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
def test_active_connections_are_not_cleared_on_body_close_during_transaction
ActiveRecord::Base.transaction do
_, _, body = @management.call(@env)
body.close
- assert ActiveRecord::Base.connection_handler.active_connections?
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
end
@@ -62,7 +62,7 @@ module ActiveRecord
app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
explosive = middleware(app)
assert_raises(NotImplementedError) { explosive.call(@env) }
- assert !ActiveRecord::Base.connection_handler.active_connections?
+ assert_not_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
def test_connections_not_closed_if_exception_inside_transaction
@@ -70,14 +70,14 @@ module ActiveRecord
app = Class.new(App) { def call(env); raise RuntimeError; end }.new
explosive = middleware(app)
assert_raises(RuntimeError) { explosive.call(@env) }
- assert ActiveRecord::Base.connection_handler.active_connections?
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
end
test "doesn't clear active connections when running in a test case" do
executor.wrap do
@management.call(@env)
- assert ActiveRecord::Base.connection_handler.active_connections?
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
end
@@ -85,7 +85,7 @@ module ActiveRecord
body = Class.new(String) { def to_path; "/path"; end }.new
app = lambda { |_| [200, {}, body] }
response_body = middleware(app).call(@env)[2]
- assert response_body.respond_to?(:to_path)
+ assert_respond_to response_body, :to_path
assert_equal "/path", response_body.to_path
end
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index 2bfe490602..9ac03629c3 100644
--- a/activerecord/test/cases/connection_pool_test.rb
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -35,12 +35,12 @@ module ActiveRecord
def test_checkout_after_close
connection = pool.connection
- assert connection.in_use?
+ assert_predicate connection, :in_use?
connection.close
- assert !connection.in_use?
+ assert_not_predicate connection, :in_use?
- assert pool.connection.in_use?
+ assert_predicate pool.connection, :in_use?
end
def test_released_connection_moves_between_threads
@@ -80,18 +80,18 @@ module ActiveRecord
end
def test_active_connection_in_use
- assert !pool.active_connection?
+ assert_not_predicate pool, :active_connection?
main_thread = pool.connection
- assert pool.active_connection?
+ assert_predicate pool, :active_connection?
main_thread.close
- assert !pool.active_connection?
+ assert_not_predicate pool, :active_connection?
end
def test_full_pool_exception
- @pool.size.times { @pool.checkout }
+ @pool.size.times { assert @pool.checkout }
assert_raises(ConnectionTimeoutError) do
@pool.checkout
end
@@ -156,13 +156,60 @@ module ActiveRecord
@pool.connections.each { |conn| conn.close if conn.in_use? }
end
+ def test_flush
+ idle_conn = @pool.checkout
+ recent_conn = @pool.checkout
+ active_conn = @pool.checkout
+
+ @pool.checkin idle_conn
+ @pool.checkin recent_conn
+
+ assert_equal 3, @pool.connections.length
+
+ def idle_conn.seconds_idle
+ 1000
+ end
+
+ @pool.flush(30)
+
+ assert_equal 2, @pool.connections.length
+
+ assert_equal [recent_conn, active_conn].sort_by(&:__id__), @pool.connections.sort_by(&:__id__)
+ ensure
+ @pool.checkin active_conn
+ end
+
+ def test_flush_bang
+ idle_conn = @pool.checkout
+ recent_conn = @pool.checkout
+ active_conn = @pool.checkout
+ _dead_conn = Thread.new { @pool.checkout }.join
+
+ @pool.checkin idle_conn
+ @pool.checkin recent_conn
+
+ assert_equal 4, @pool.connections.length
+
+ def idle_conn.seconds_idle
+ 1000
+ end
+
+ @pool.flush!
+
+ assert_equal 1, @pool.connections.length
+
+ assert_equal [active_conn].sort_by(&:__id__), @pool.connections.sort_by(&:__id__)
+ ensure
+ @pool.checkin active_conn
+ end
+
def test_remove_connection
conn = @pool.checkout
- assert conn.in_use?
+ assert_predicate conn, :in_use?
length = @pool.connections.length
@pool.remove conn
- assert conn.in_use?
+ assert_predicate conn, :in_use?
assert_equal(length - 1, @pool.connections.length)
ensure
conn.close
@@ -177,11 +224,11 @@ module ActiveRecord
end
def test_active_connection?
- assert !@pool.active_connection?
+ assert_not_predicate @pool, :active_connection?
assert @pool.connection
- assert @pool.active_connection?
+ assert_predicate @pool, :active_connection?
@pool.release_connection
- assert !@pool.active_connection?
+ assert_not_predicate @pool, :active_connection?
end
def test_checkout_behaviour
@@ -205,6 +252,14 @@ module ActiveRecord
end.join
end
+ def test_checkout_order_is_lifo
+ conn1 = @pool.checkout
+ conn2 = @pool.checkout
+ @pool.checkin conn1
+ @pool.checkin conn2
+ assert_equal [conn2, conn1], 2.times.map { @pool.checkout }
+ end
+
# The connection pool is "fair" if threads waiting for
# connections receive them in the order in which they began
# waiting. This ensures that we don't timeout one HTTP request
@@ -414,6 +469,7 @@ module ActiveRecord
end
def test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns
+ Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception
@pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout
[:disconnect, :clear_reloadable_connections].each do |group_action_method|
@pool.with_connection do |connection|
@@ -422,6 +478,8 @@ module ActiveRecord
end
end
end
+ ensure
+ Thread.report_on_exception = original_report_on_exception
end
def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns
@@ -438,7 +496,7 @@ module ActiveRecord
assert_nil timed_join_result
# assert that since this is within default timeout our connection hasn't been forcefully taken away from us
- assert @pool.active_connection?
+ assert_predicate @pool, :active_connection?
end
ensure
thread.join if thread && !timed_join_result # clean up the other thread
@@ -452,7 +510,7 @@ module ActiveRecord
@pool.with_connection do |connection|
Thread.new { @pool.send(group_action_method) }.join
# assert connection has been forcefully taken away from us
- assert_not @pool.active_connection?
+ assert_not_predicate @pool, :active_connection?
# make a new connection for with_connection to clean up
@pool.connection
diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb
index 3fa0ca8366..5b80f16a44 100644
--- a/activerecord/test/cases/connection_specification/resolver_test.rb
+++ b/activerecord/test/cases/connection_specification/resolver_test.rb
@@ -19,7 +19,7 @@ module ActiveRecord
spec "ridiculous://foo?encoding=utf8"
end
- assert_match "Could not load 'active_record/connection_adapters/ridiculous_adapter'", error.message
+ assert_match "Could not load the 'ridiculous' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", error.message
end
# The abstract adapter is used simply to bypass the bit of code that
diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb
index 356afdbd2b..6e7ae2efb4 100644
--- a/activerecord/test/cases/core_test.rb
+++ b/activerecord/test/cases/core_test.rb
@@ -4,7 +4,6 @@ require "cases/helper"
require "models/person"
require "models/topic"
require "pp"
-require "active_support/core_ext/string/strip"
class NonExistentTable < ActiveRecord::Base; end
@@ -39,26 +38,26 @@ class CoreTest < ActiveRecord::TestCase
topic = Topic.new
actual = "".dup
PP.pp(topic, StringIO.new(actual))
- expected = <<-PRETTY.strip_heredoc
- #<Topic:0xXXXXXX
- id: nil,
- title: nil,
- author_name: nil,
- author_email_address: "test@test.com",
- written_on: nil,
- bonus_time: nil,
- last_read: nil,
- content: nil,
- important: nil,
- approved: true,
- replies_count: 0,
- unique_replies_count: 0,
- parent_id: nil,
- parent_title: nil,
- type: nil,
- group: nil,
- created_at: nil,
- updated_at: nil>
+ expected = <<~PRETTY
+ #<Topic:0xXXXXXX
+ id: nil,
+ title: nil,
+ author_name: nil,
+ author_email_address: "test@test.com",
+ written_on: nil,
+ bonus_time: nil,
+ last_read: nil,
+ content: nil,
+ important: nil,
+ approved: true,
+ replies_count: 0,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: nil,
+ updated_at: nil>
PRETTY
assert actual.start_with?(expected.split("XXXXXX").first)
assert actual.end_with?(expected.split("XXXXXX").last)
@@ -68,26 +67,26 @@ class CoreTest < ActiveRecord::TestCase
topic = topics(:first)
actual = "".dup
PP.pp(topic, StringIO.new(actual))
- expected = <<-PRETTY.strip_heredoc
- #<Topic:0x\\w+
- id: 1,
- title: "The First Topic",
- author_name: "David",
- author_email_address: "david@loudthinking.com",
- written_on: 2003-07-16 14:28:11 UTC,
- bonus_time: 2000-01-01 14:28:00 UTC,
- last_read: Thu, 15 Apr 2004,
- content: "Have a nice day",
- important: nil,
- approved: false,
- replies_count: 1,
- unique_replies_count: 0,
- parent_id: nil,
- parent_title: nil,
- type: nil,
- group: nil,
- created_at: [^,]+,
- updated_at: [^,>]+>
+ expected = <<~PRETTY
+ #<Topic:0x\\w+
+ id: 1,
+ title: "The First Topic",
+ author_name: "David",
+ author_email_address: "david@loudthinking.com",
+ written_on: 2003-07-16 14:28:11 UTC,
+ bonus_time: 2000-01-01 14:28:00 UTC,
+ last_read: Thu, 15 Apr 2004,
+ content: "Have a nice day",
+ important: nil,
+ approved: false,
+ replies_count: 1,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: [^,]+,
+ updated_at: [^,>]+>
PRETTY
assert_match(/\A#{expected}\z/, actual)
end
diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb
index 51f6164138..e64a8372d0 100644
--- a/activerecord/test/cases/date_time_precision_test.rb
+++ b/activerecord/test/cases/date_time_precision_test.rb
@@ -27,6 +27,24 @@ if subsecond_precision_supported?
assert_equal 5, Foo.columns_hash["updated_at"].precision
end
+ def test_datetime_precision_is_truncated_on_assignment
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :created_at, :datetime, precision: 0
+ @connection.add_column :foos, :updated_at, :datetime, precision: 6
+
+ time = ::Time.now.change(nsec: 123456789)
+ foo = Foo.new(created_at: time, updated_at: time)
+
+ assert_equal 0, foo.created_at.nsec
+ assert_equal 123456000, foo.updated_at.nsec
+
+ foo.save!
+ foo.reload
+
+ assert_equal 0, foo.created_at.nsec
+ assert_equal 123456000, foo.updated_at.nsec
+ end
+
def test_timestamps_helper_with_custom_precision
@connection.create_table(:foos, force: true) do |t|
t.timestamps precision: 4
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 4690682cd8..3d11b573f1 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -53,7 +53,7 @@ class DefaultNumbersTest < ActiveRecord::TestCase
def test_default_decimal_number
record = DefaultNumber.new
- assert_equal BigDecimal.new("2.78"), record.decimal_number
+ assert_equal BigDecimal("2.78"), record.decimal_number
assert_equal "2.78", record.decimal_number_before_type_cast
end
end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index a602f83d8c..83cc2aa319 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -24,18 +24,18 @@ class DirtyTest < ActiveRecord::TestCase
# Change catchphrase.
pirate.catchphrase = "arrr"
- assert pirate.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
assert_nil pirate.catchphrase_was
assert_equal [nil, "arrr"], pirate.catchphrase_change
# Saved - no changes.
pirate.save!
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
assert_nil pirate.catchphrase_change
# Same value - no changes.
pirate.catchphrase = "arrr"
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
assert_nil pirate.catchphrase_change
end
@@ -46,23 +46,23 @@ class DirtyTest < ActiveRecord::TestCase
# New record - no changes.
pirate = target.new
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Saved - no changes.
pirate.catchphrase = "arrrr, time zone!!"
pirate.save!
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Change created_on.
old_created_on = pirate.created_on
pirate.created_on = Time.now - 1.day
- assert pirate.created_on_changed?
+ assert_predicate pirate, :created_on_changed?
assert_kind_of ActiveSupport::TimeWithZone, pirate.created_on_was
assert_equal old_created_on, pirate.created_on_was
pirate.created_on = old_created_on
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
end
end
@@ -73,7 +73,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate = target.create!
pirate.created_on = pirate.created_on
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
end
end
@@ -86,19 +86,19 @@ class DirtyTest < ActiveRecord::TestCase
# New record - no changes.
pirate = target.new
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Saved - no changes.
pirate.catchphrase = "arrrr, time zone!!"
pirate.save!
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Change created_on.
old_created_on = pirate.created_on
pirate.created_on = Time.now + 1.day
- assert pirate.created_on_changed?
+ assert_predicate pirate, :created_on_changed?
# kind_of does not work because
# ActiveSupport::TimeWithZone.name == 'Time'
assert_instance_of Time, pirate.created_on_was
@@ -113,19 +113,19 @@ class DirtyTest < ActiveRecord::TestCase
# New record - no changes.
pirate = target.new
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Saved - no changes.
pirate.catchphrase = "arrrr, time zone!!"
pirate.save!
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Change created_on.
old_created_on = pirate.created_on
pirate.created_on = Time.now + 1.day
- assert pirate.created_on_changed?
+ assert_predicate pirate, :created_on_changed?
# kind_of does not work because
# ActiveSupport::TimeWithZone.name == 'Time'
assert_instance_of Time, pirate.created_on_was
@@ -137,11 +137,11 @@ class DirtyTest < ActiveRecord::TestCase
# the actual attribute here is name, title is an
# alias setup via alias_attribute
parrot = Parrot.new
- assert !parrot.title_changed?
+ assert_not_predicate parrot, :title_changed?
assert_nil parrot.title_change
parrot.name = "Sam"
- assert parrot.title_changed?
+ assert_predicate parrot, :title_changed?
assert_nil parrot.title_was
assert_equal parrot.name_change, parrot.title_change
end
@@ -153,7 +153,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate.restore_catchphrase!
assert_equal "Yar!", pirate.catchphrase
assert_equal Hash.new, pirate.changes
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
end
def test_nullable_number_not_marked_as_changed_if_new_value_is_blank
@@ -161,7 +161,7 @@ class DirtyTest < ActiveRecord::TestCase
["", nil].each do |value|
pirate.parrot_id = value
- assert !pirate.parrot_id_changed?
+ assert_not_predicate pirate, :parrot_id_changed?
assert_nil pirate.parrot_id_change
end
end
@@ -171,7 +171,7 @@ class DirtyTest < ActiveRecord::TestCase
["", nil].each do |value|
numeric_data.bank_balance = value
- assert !numeric_data.bank_balance_changed?
+ assert_not_predicate numeric_data, :bank_balance_changed?
assert_nil numeric_data.bank_balance_change
end
end
@@ -181,7 +181,7 @@ class DirtyTest < ActiveRecord::TestCase
["", nil].each do |value|
numeric_data.temperature = value
- assert !numeric_data.temperature_changed?
+ assert_not_predicate numeric_data, :temperature_changed?
assert_nil numeric_data.temperature_change
end
end
@@ -197,7 +197,7 @@ class DirtyTest < ActiveRecord::TestCase
["", nil].each do |value|
topic.written_on = value
assert_nil topic.written_on
- assert !topic.written_on_changed?
+ assert_not_predicate topic, :written_on_changed?
end
end
end
@@ -208,10 +208,10 @@ class DirtyTest < ActiveRecord::TestCase
pirate.catchphrase = "arrr"
assert pirate.save!
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
pirate.parrot_id = "0"
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
def test_integer_zero_to_integer_zero_not_marked_as_changed
@@ -220,17 +220,17 @@ class DirtyTest < ActiveRecord::TestCase
pirate.catchphrase = "arrr"
assert pirate.save!
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
pirate.parrot_id = 0
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
def test_float_zero_to_string_zero_not_marked_as_changed
data = NumericData.new temperature: 0.0
data.save!
- assert_not data.changed?
+ assert_not_predicate data, :changed?
data.temperature = "0"
assert_empty data.changes
@@ -251,38 +251,38 @@ class DirtyTest < ActiveRecord::TestCase
# check the change from 1 to ''
pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties")
pirate.parrot_id = ""
- assert pirate.parrot_id_changed?
+ assert_predicate pirate, :parrot_id_changed?
assert_equal([1, nil], pirate.parrot_id_change)
pirate.save
# check the change from nil to 0
pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties")
pirate.parrot_id = 0
- assert pirate.parrot_id_changed?
+ assert_predicate pirate, :parrot_id_changed?
assert_equal([nil, 0], pirate.parrot_id_change)
pirate.save
# check the change from 0 to ''
pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties")
pirate.parrot_id = ""
- assert pirate.parrot_id_changed?
+ assert_predicate pirate, :parrot_id_changed?
assert_equal([0, nil], pirate.parrot_id_change)
end
def test_object_should_be_changed_if_any_attribute_is_changed
pirate = Pirate.new
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
assert_equal [], pirate.changed
assert_equal Hash.new, pirate.changes
pirate.catchphrase = "arrr"
- assert pirate.changed?
+ assert_predicate pirate, :changed?
assert_nil pirate.catchphrase_was
assert_equal %w(catchphrase), pirate.changed
assert_equal({ "catchphrase" => [nil, "arrr"] }, pirate.changes)
pirate.save
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
assert_equal [], pirate.changed
assert_equal Hash.new, pirate.changes
end
@@ -290,40 +290,40 @@ class DirtyTest < ActiveRecord::TestCase
def test_attribute_will_change!
pirate = Pirate.create!(catchphrase: "arr")
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
assert pirate.catchphrase_will_change!
- assert pirate.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
assert_equal ["arr", "arr"], pirate.catchphrase_change
pirate.catchphrase << " matey!"
- assert pirate.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
assert_equal ["arr", "arr matey!"], pirate.catchphrase_change
end
def test_virtual_attribute_will_change
parrot = Parrot.create!(name: "Ruby")
parrot.send(:attribute_will_change!, :cancel_save_from_callback)
- assert parrot.has_changes_to_save?
+ assert_predicate parrot, :has_changes_to_save?
end
def test_association_assignment_changes_foreign_key
pirate = Pirate.create!(catchphrase: "jarl")
pirate.parrot = Parrot.create!(name: "Lorre")
- assert pirate.changed?
+ assert_predicate pirate, :changed?
assert_equal %w(parrot_id), pirate.changed
end
def test_attribute_should_be_compared_with_type_cast
topic = Topic.new
- assert topic.approved?
- assert !topic.approved_changed?
+ assert_predicate topic, :approved?
+ assert_not_predicate topic, :approved_changed?
# Coming from web form.
params = { topic: { approved: 1 } }
# In the controller.
topic.attributes = params[:topic]
- assert topic.approved?
- assert !topic.approved_changed?
+ assert_predicate topic, :approved?
+ assert_not_predicate topic, :approved_changed?
end
def test_partial_update
@@ -366,7 +366,7 @@ class DirtyTest < ActiveRecord::TestCase
def test_changed_attributes_should_be_preserved_if_save_failure
pirate = Pirate.new
pirate.parrot_id = 1
- assert !pirate.save
+ assert_not pirate.save
check_pirate_after_save_failure(pirate)
pirate = Pirate.new
@@ -378,9 +378,9 @@ class DirtyTest < ActiveRecord::TestCase
def test_reload_should_clear_changed_attributes
pirate = Pirate.create!(catchphrase: "shiver me timbers")
pirate.catchphrase = "*hic*"
- assert pirate.changed?
+ assert_predicate pirate, :changed?
pirate.reload
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
def test_dup_objects_should_not_copy_dirty_flag_from_creator
@@ -388,17 +388,17 @@ class DirtyTest < ActiveRecord::TestCase
pirate_dup = pirate.dup
pirate_dup.restore_catchphrase!
pirate.catchphrase = "I love Rum"
- assert pirate.catchphrase_changed?
- assert !pirate_dup.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
+ assert_not_predicate pirate_dup, :catchphrase_changed?
end
def test_reverted_changes_are_not_dirty
phrase = "shiver me timbers"
pirate = Pirate.create!(catchphrase: phrase)
pirate.catchphrase = "*hic*"
- assert pirate.changed?
+ assert_predicate pirate, :changed?
pirate.catchphrase = phrase
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
def test_reverted_changes_are_not_dirty_after_multiple_changes
@@ -406,40 +406,40 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.create!(catchphrase: phrase)
10.times do |i|
pirate.catchphrase = "*hic*" * i
- assert pirate.changed?
+ assert_predicate pirate, :changed?
end
- assert pirate.changed?
+ assert_predicate pirate, :changed?
pirate.catchphrase = phrase
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
def test_reverted_changes_are_not_dirty_going_from_nil_to_value_and_back
pirate = Pirate.create!(catchphrase: "Yar!")
pirate.parrot_id = 1
- assert pirate.changed?
- assert pirate.parrot_id_changed?
- assert !pirate.catchphrase_changed?
+ assert_predicate pirate, :changed?
+ assert_predicate pirate, :parrot_id_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
pirate.parrot_id = nil
- assert !pirate.changed?
- assert !pirate.parrot_id_changed?
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :changed?
+ assert_not_predicate pirate, :parrot_id_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
end
def test_save_should_store_serialized_attributes_even_with_partial_writes
with_partial_writes(Topic) do
topic = Topic.create!(content: { a: "a" })
- assert_not topic.changed?
+ assert_not_predicate topic, :changed?
topic.content[:b] = "b"
- assert topic.changed?
+ assert_predicate topic, :changed?
topic.save!
- assert_not topic.changed?
+ assert_not_predicate topic, :changed?
assert_equal "b", topic.content[:b]
topic.reload
@@ -473,6 +473,14 @@ class DirtyTest < ActiveRecord::TestCase
end
end
+ def test_changes_to_save_should_not_mutate_array_of_hashes
+ topic = Topic.new(author_name: "Bill", content: [{ a: "a" }])
+
+ topic.changes_to_save
+
+ assert_equal [{ a: "a" }], topic.content
+ end
+
def test_previous_changes
# original values should be in previous_changes
pirate = Pirate.new
@@ -488,7 +496,7 @@ class DirtyTest < ActiveRecord::TestCase
assert_not_nil pirate.previous_changes["updated_on"][1]
assert_nil pirate.previous_changes["created_on"][0]
assert_not_nil pirate.previous_changes["created_on"][1]
- assert !pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("parrot_id")
# original values should be in previous_changes
pirate = Pirate.new
@@ -502,7 +510,7 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal [nil, pirate.id], pirate.previous_changes["id"]
assert_includes pirate.previous_changes, "updated_on"
assert_includes pirate.previous_changes, "created_on"
- assert !pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("parrot_id")
pirate.catchphrase = "Yar!!"
pirate.reload
@@ -519,8 +527,8 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal ["arrr", "Me Maties!"], pirate.previous_changes["catchphrase"]
assert_not_nil pirate.previous_changes["updated_on"][0]
assert_not_nil pirate.previous_changes["updated_on"][1]
- assert !pirate.previous_changes.key?("parrot_id")
- assert !pirate.previous_changes.key?("created_on")
+ assert_not pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("created_on")
pirate = Pirate.find_by_catchphrase("Me Maties!")
@@ -533,8 +541,8 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal ["Me Maties!", "Thar She Blows!"], pirate.previous_changes["catchphrase"]
assert_not_nil pirate.previous_changes["updated_on"][0]
assert_not_nil pirate.previous_changes["updated_on"][1]
- assert !pirate.previous_changes.key?("parrot_id")
- assert !pirate.previous_changes.key?("created_on")
+ assert_not pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("created_on")
travel(1.second)
@@ -545,8 +553,8 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal ["Thar She Blows!", "Ahoy!"], pirate.previous_changes["catchphrase"]
assert_not_nil pirate.previous_changes["updated_on"][0]
assert_not_nil pirate.previous_changes["updated_on"][1]
- assert !pirate.previous_changes.key?("parrot_id")
- assert !pirate.previous_changes.key?("created_on")
+ assert_not pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("created_on")
travel(1.second)
@@ -557,8 +565,8 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes["catchphrase"]
assert_not_nil pirate.previous_changes["updated_on"][0]
assert_not_nil pirate.previous_changes["updated_on"][1]
- assert !pirate.previous_changes.key?("parrot_id")
- assert !pirate.previous_changes.key?("created_on")
+ assert_not pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("created_on")
ensure
travel_back
end
@@ -596,7 +604,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.create!(catchphrase: "rrrr", created_on: time_in_paris)
pirate.created_on = pirate.created_on.in_time_zone("Tokyo").to_s
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
end
test "partial insert" do
@@ -627,7 +635,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.create!(catchphrase: "arrrr")
pirate.catchphrase << " matey!"
- assert pirate.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
expected_changes = {
"catchphrase" => ["arrrr", "arrrr matey!"]
}
@@ -641,7 +649,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate.reload
assert_equal "arrrr matey!", pirate.catchphrase
- assert_not pirate.changed?
+ assert_not_predicate pirate, :changed?
end
test "in place mutation for binary" do
@@ -652,19 +660,19 @@ class DirtyTest < ActiveRecord::TestCase
binary = klass.create!(data: "\\\\foo")
- assert_not binary.changed?
+ assert_not_predicate binary, :changed?
binary.data = binary.data.dup
- assert_not binary.changed?
+ assert_not_predicate binary, :changed?
binary = klass.last
- assert_not binary.changed?
+ assert_not_predicate binary, :changed?
binary.data << "bar"
- assert binary.changed?
+ assert_predicate binary, :changed?
end
test "changes is correct for subclass" do
@@ -679,7 +687,7 @@ class DirtyTest < ActiveRecord::TestCase
new_catchphrase = "arrrr matey!"
pirate.catchphrase = new_catchphrase
- assert pirate.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
expected_changes = {
"catchphrase" => ["arrrr", new_catchphrase]
@@ -698,7 +706,7 @@ class DirtyTest < ActiveRecord::TestCase
new_catchphrase = "arrrr matey!"
pirate.catchphrase = new_catchphrase
- assert pirate.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
expected_changes = {
"catchphrase" => ["arrrr", new_catchphrase]
@@ -720,7 +728,7 @@ class DirtyTest < ActiveRecord::TestCase
end
model = klass.new(first_name: "Jim")
- assert model.first_name_changed?
+ assert_predicate model, :first_name_changed?
end
test "attribute_will_change! doesn't try to save non-persistable attributes" do
@@ -732,10 +740,28 @@ class DirtyTest < ActiveRecord::TestCase
record = klass.new(first_name: "Sean")
record.non_persisted_attribute_will_change!
- assert record.non_persisted_attribute_changed?
+ assert_predicate record, :non_persisted_attribute_changed?
assert record.save
end
+ test "virtual attributes are not written with partial_writes off" do
+ with_partial_writes(ActiveRecord::Base, false) do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "people"
+ attribute :non_persisted_attribute, :string
+ end
+
+ record = klass.new(first_name: "Sean")
+ record.non_persisted_attribute_will_change!
+
+ assert record.save
+
+ record.non_persisted_attribute_will_change!
+
+ assert record.save
+ end
+ end
+
test "mutating and then assigning doesn't remove the change" do
pirate = Pirate.create!(catchphrase: "arrrr")
pirate.catchphrase << " matey!"
@@ -762,26 +788,33 @@ class DirtyTest < ActiveRecord::TestCase
test "attributes assigned but not selected are dirty" do
person = Person.select(:id).first
- refute person.changed?
+ assert_not_predicate person, :changed?
person.first_name = "Sean"
- assert person.changed?
+ assert_predicate person, :changed?
person.first_name = nil
- assert person.changed?
+ assert_predicate person, :changed?
+ end
+
+ test "attributes not selected are still missing after save" do
+ person = Person.select(:id).first
+ assert_raises(ActiveModel::MissingAttributeError) { person.first_name }
+ assert person.save # calls forget_attribute_assignments
+ assert_raises(ActiveModel::MissingAttributeError) { person.first_name }
end
test "saved_change_to_attribute? returns whether a change occurred in the last save" do
person = Person.create!(first_name: "Sean")
- assert person.saved_change_to_first_name?
- refute person.saved_change_to_gender?
+ assert_predicate person, :saved_change_to_first_name?
+ assert_not_predicate person, :saved_change_to_gender?
assert person.saved_change_to_first_name?(from: nil, to: "Sean")
assert person.saved_change_to_first_name?(from: nil)
assert person.saved_change_to_first_name?(to: "Sean")
- refute person.saved_change_to_first_name?(from: "Jim", to: "Sean")
- refute person.saved_change_to_first_name?(from: "Jim")
- refute person.saved_change_to_first_name?(to: "Jim")
+ assert_not person.saved_change_to_first_name?(from: "Jim", to: "Sean")
+ assert_not person.saved_change_to_first_name?(from: "Jim")
+ assert_not person.saved_change_to_first_name?(to: "Jim")
end
test "saved_change_to_attribute returns the change that occurred in the last save" do
@@ -816,11 +849,11 @@ class DirtyTest < ActiveRecord::TestCase
test "saved_changes? returns whether the last call to save changed anything" do
person = Person.create!(first_name: "Sean")
- assert person.saved_changes?
+ assert_predicate person, :saved_changes?
person.save
- refute person.saved_changes?
+ assert_not_predicate person, :saved_changes?
end
test "saved_changes returns a hash of all the changes that occurred" do
@@ -850,7 +883,7 @@ class DirtyTest < ActiveRecord::TestCase
end
person = klass.create!(first_name: "Sean")
- refute person.changed?
+ assert_not_predicate person, :changed?
end
private
@@ -863,8 +896,8 @@ class DirtyTest < ActiveRecord::TestCase
end
def check_pirate_after_save_failure(pirate)
- assert pirate.changed?
- assert pirate.parrot_id_changed?
+ assert_predicate pirate, :changed?
+ assert_predicate pirate, :parrot_id_changed?
assert_equal %w(parrot_id), pirate.changed
assert_nil pirate.parrot_id_was
end
diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb
index 2fefdbf204..387a0b1fdd 100644
--- a/activerecord/test/cases/dup_test.rb
+++ b/activerecord/test/cases/dup_test.rb
@@ -3,13 +3,14 @@
require "cases/helper"
require "models/reply"
require "models/topic"
+require "models/movie"
module ActiveRecord
class DupTest < ActiveRecord::TestCase
fixtures :topics
def test_dup
- assert !Topic.new.freeze.dup.frozen?
+ assert_not_predicate Topic.new.freeze.dup, :frozen?
end
def test_not_readonly
@@ -40,7 +41,7 @@ module ActiveRecord
topic.destroy
duped = topic.dup
- assert_not duped.destroyed?
+ assert_not_predicate duped, :destroyed?
end
def test_dup_has_no_id
@@ -62,10 +63,10 @@ module ActiveRecord
topic.attributes = dbtopic.attributes.except("id")
- #duped has no timestamp values
+ # duped has no timestamp values
duped = dbtopic.dup
- #clear topic timestamp values
+ # clear topic timestamp values
topic.send(:clear_timestamp_attributes)
assert_equal topic.changes, duped.changes
@@ -100,7 +101,7 @@ module ActiveRecord
# temporary change to the topic object
topic.updated_at -= 3.days
- #dup should not preserve the timestamps if present
+ # dup should not preserve the timestamps if present
new_topic = topic.dup
assert_nil new_topic.updated_at
assert_nil new_topic.created_at
@@ -126,12 +127,12 @@ module ActiveRecord
duped = topic.dup
duped.title = nil
- assert duped.invalid?
+ assert_predicate duped, :invalid?
topic.title = nil
duped.title = "Mathematics"
- assert topic.invalid?
- assert duped.valid?
+ assert_predicate topic, :invalid?
+ assert_predicate duped, :valid?
end
end
@@ -157,5 +158,20 @@ module ActiveRecord
record.dup
end
end
+
+ def test_dup_record_not_persisted_after_rollback_transaction
+ movie = Movie.new(name: "test")
+
+ assert_raises(ActiveRecord::RecordInvalid) do
+ Movie.transaction do
+ movie.save!
+ duped = movie.dup
+ duped.name = nil
+ duped.save!
+ end
+ end
+
+ assert_not movie.persisted?
+ end
end
end
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
index 78cb89ccc5..d5a1d11e12 100644
--- a/activerecord/test/cases/enum_test.rb
+++ b/activerecord/test/cases/enum_test.rb
@@ -12,16 +12,16 @@ class EnumTest < ActiveRecord::TestCase
end
test "query state by predicate" do
- assert @book.published?
- assert_not @book.written?
- assert_not @book.proposed?
+ assert_predicate @book, :published?
+ assert_not_predicate @book, :written?
+ assert_not_predicate @book, :proposed?
- assert @book.read?
- assert @book.in_english?
- assert @book.author_visibility_visible?
- assert @book.illustrator_visibility_visible?
- assert @book.with_medium_font_size?
- assert @book.medium_to_read?
+ assert_predicate @book, :read?
+ assert_predicate @book, :in_english?
+ assert_predicate @book, :author_visibility_visible?
+ assert_predicate @book, :illustrator_visibility_visible?
+ assert_predicate @book, :with_medium_font_size?
+ assert_predicate @book, :medium_to_read?
end
test "query state with strings" do
@@ -76,46 +76,46 @@ class EnumTest < ActiveRecord::TestCase
end
test "build from scope" do
- assert Book.written.build.written?
- assert_not Book.written.build.proposed?
+ assert_predicate Book.written.build, :written?
+ assert_not_predicate Book.written.build, :proposed?
end
test "build from where" do
- assert Book.where(status: Book.statuses[:written]).build.written?
- assert_not Book.where(status: Book.statuses[:written]).build.proposed?
- assert Book.where(status: :written).build.written?
- assert_not Book.where(status: :written).build.proposed?
- assert Book.where(status: "written").build.written?
- assert_not Book.where(status: "written").build.proposed?
+ assert_predicate Book.where(status: Book.statuses[:written]).build, :written?
+ assert_not_predicate Book.where(status: Book.statuses[:written]).build, :proposed?
+ assert_predicate Book.where(status: :written).build, :written?
+ assert_not_predicate Book.where(status: :written).build, :proposed?
+ assert_predicate Book.where(status: "written").build, :written?
+ assert_not_predicate Book.where(status: "written").build, :proposed?
end
test "update by declaration" do
@book.written!
- assert @book.written?
+ assert_predicate @book, :written?
@book.in_english!
- assert @book.in_english?
+ assert_predicate @book, :in_english?
@book.author_visibility_visible!
- assert @book.author_visibility_visible?
+ assert_predicate @book, :author_visibility_visible?
end
test "update by setter" do
@book.update! status: :written
- assert @book.written?
+ assert_predicate @book, :written?
end
test "enum methods are overwritable" do
assert_equal "do publish work...", @book.published!
- assert @book.published?
+ assert_predicate @book, :published?
end
test "direct assignment" do
@book.status = :written
- assert @book.written?
+ assert_predicate @book, :written?
end
test "assign string value" do
@book.status = "written"
- assert @book.written?
+ assert_predicate @book, :written?
end
test "enum changed attributes" do
@@ -242,17 +242,17 @@ class EnumTest < ActiveRecord::TestCase
end
test "building new objects with enum scopes" do
- assert Book.written.build.written?
- assert Book.read.build.read?
- assert Book.in_spanish.build.in_spanish?
- assert Book.illustrator_visibility_invisible.build.illustrator_visibility_invisible?
+ assert_predicate Book.written.build, :written?
+ assert_predicate Book.read.build, :read?
+ assert_predicate Book.in_spanish.build, :in_spanish?
+ assert_predicate Book.illustrator_visibility_invisible.build, :illustrator_visibility_invisible?
end
test "creating new objects with enum scopes" do
- assert Book.written.create.written?
- assert Book.read.create.read?
- assert Book.in_spanish.create.in_spanish?
- assert Book.illustrator_visibility_invisible.create.illustrator_visibility_invisible?
+ assert_predicate Book.written.create, :written?
+ assert_predicate Book.read.create, :read?
+ assert_predicate Book.in_spanish.create, :in_spanish?
+ assert_predicate Book.illustrator_visibility_invisible.create, :illustrator_visibility_invisible?
end
test "_before_type_cast" do
@@ -308,6 +308,24 @@ class EnumTest < ActiveRecord::TestCase
end
end
+ test "reserved enum values for relation" do
+ relation_method_samples = [
+ :records,
+ :to_ary,
+ :scope_for_create
+ ]
+
+ relation_method_samples.each do |value|
+ e = assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum category: [:other, value]
+ end
+ end
+ assert_match(/You tried to define an enum named .* on the model/, e.message)
+ end
+ end
+
test "overriding enum method should not raise" do
assert_nothing_raised do
Class.new(ActiveRecord::Base) do
@@ -337,9 +355,9 @@ class EnumTest < ActiveRecord::TestCase
klass.delete_all
klass.create!(status: "proposed")
book = klass.new(status: "written")
- assert book.valid?
+ assert_predicate book, :valid?
book.status = "proposed"
- assert_not book.valid?
+ assert_not_predicate book, :valid?
end
test "validate inclusion of value in array" do
@@ -350,9 +368,9 @@ class EnumTest < ActiveRecord::TestCase
end
klass.delete_all
invalid_book = klass.new(status: "proposed")
- assert_not invalid_book.valid?
+ assert_not_predicate invalid_book, :valid?
valid_book = klass.new(status: "written")
- assert valid_book.valid?
+ assert_predicate valid_book, :valid?
end
test "enums are distinct per class" do
@@ -399,10 +417,10 @@ class EnumTest < ActiveRecord::TestCase
end
book1 = klass.proposed.create!
- assert book1.proposed?
+ assert_predicate book1, :proposed?
book2 = klass.single.create!
- assert book2.single?
+ assert_predicate book2, :single?
end
test "enum with alias_attribute" do
@@ -413,62 +431,62 @@ class EnumTest < ActiveRecord::TestCase
end
book = klass.proposed.create!
- assert book.proposed?
+ assert_predicate book, :proposed?
assert_equal "proposed", book.aliased_status
book = klass.find(book.id)
- assert book.proposed?
+ assert_predicate book, :proposed?
assert_equal "proposed", book.aliased_status
end
test "query state by predicate with prefix" do
- assert @book.author_visibility_visible?
- assert_not @book.author_visibility_invisible?
- assert @book.illustrator_visibility_visible?
- assert_not @book.illustrator_visibility_invisible?
+ assert_predicate @book, :author_visibility_visible?
+ assert_not_predicate @book, :author_visibility_invisible?
+ assert_predicate @book, :illustrator_visibility_visible?
+ assert_not_predicate @book, :illustrator_visibility_invisible?
end
test "query state by predicate with custom prefix" do
- assert @book.in_english?
- assert_not @book.in_spanish?
- assert_not @book.in_french?
+ assert_predicate @book, :in_english?
+ assert_not_predicate @book, :in_spanish?
+ assert_not_predicate @book, :in_french?
end
test "query state by predicate with custom suffix" do
- assert @book.medium_to_read?
- assert_not @book.easy_to_read?
- assert_not @book.hard_to_read?
+ assert_predicate @book, :medium_to_read?
+ assert_not_predicate @book, :easy_to_read?
+ assert_not_predicate @book, :hard_to_read?
end
test "enum methods with custom suffix defined" do
- assert @book.class.respond_to?(:easy_to_read)
- assert @book.class.respond_to?(:medium_to_read)
- assert @book.class.respond_to?(:hard_to_read)
+ assert_respond_to @book.class, :easy_to_read
+ assert_respond_to @book.class, :medium_to_read
+ assert_respond_to @book.class, :hard_to_read
- assert @book.respond_to?(:easy_to_read?)
- assert @book.respond_to?(:medium_to_read?)
- assert @book.respond_to?(:hard_to_read?)
+ assert_respond_to @book, :easy_to_read?
+ assert_respond_to @book, :medium_to_read?
+ assert_respond_to @book, :hard_to_read?
- assert @book.respond_to?(:easy_to_read!)
- assert @book.respond_to?(:medium_to_read!)
- assert @book.respond_to?(:hard_to_read!)
+ assert_respond_to @book, :easy_to_read!
+ assert_respond_to @book, :medium_to_read!
+ assert_respond_to @book, :hard_to_read!
end
test "update enum attributes with custom suffix" do
@book.medium_to_read!
- assert_not @book.easy_to_read?
- assert @book.medium_to_read?
- assert_not @book.hard_to_read?
+ assert_not_predicate @book, :easy_to_read?
+ assert_predicate @book, :medium_to_read?
+ assert_not_predicate @book, :hard_to_read?
@book.easy_to_read!
- assert @book.easy_to_read?
- assert_not @book.medium_to_read?
- assert_not @book.hard_to_read?
+ assert_predicate @book, :easy_to_read?
+ assert_not_predicate @book, :medium_to_read?
+ assert_not_predicate @book, :hard_to_read?
@book.hard_to_read!
- assert_not @book.easy_to_read?
- assert_not @book.medium_to_read?
- assert @book.hard_to_read?
+ assert_not_predicate @book, :easy_to_read?
+ assert_not_predicate @book, :medium_to_read?
+ assert_predicate @book, :hard_to_read?
end
test "uses default status when no status is provided in fixtures" do
@@ -479,12 +497,12 @@ class EnumTest < ActiveRecord::TestCase
test "uses default value from database on initialization" do
book = Book.new
- assert book.proposed?
+ assert_predicate book, :proposed?
end
test "uses default value from database on initialization when using custom mapping" do
book = Book.new
- assert book.hard?
+ assert_predicate book, :hard?
end
test "data type of Enum type" do
diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb
index fb698c47cd..82cc891970 100644
--- a/activerecord/test/cases/explain_subscriber_test.rb
+++ b/activerecord/test/cases/explain_subscriber_test.rb
@@ -15,20 +15,20 @@ if ActiveRecord::Base.connection.supports_explain?
def test_collects_nothing_if_the_payload_has_an_exception
SUBSCRIBER.finish(nil, nil, exception: Exception.new)
- assert queries.empty?
+ assert_empty queries
end
def test_collects_nothing_for_ignored_payloads
ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip|
SUBSCRIBER.finish(nil, nil, name: ip)
end
- assert queries.empty?
+ assert_empty queries
end
def test_collects_nothing_if_collect_is_false
ActiveRecord::ExplainRegistry.collect = false
SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "select 1 from users", binds: [1, 2])
- assert queries.empty?
+ assert_empty queries
end
def test_collects_pairs_of_queries_and_binds
@@ -42,12 +42,12 @@ if ActiveRecord::Base.connection.supports_explain?
def test_collects_nothing_if_the_statement_is_not_whitelisted
SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "SHOW max_identifier_length")
- assert queries.empty?
+ assert_empty queries
end
def test_collects_nothing_if_the_statement_is_only_partially_matched
SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "select_db yo_mama")
- assert queries.empty?
+ assert_empty queries
end
def test_collects_cte_queries
diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb
index 17654027a9..a0e75f4e89 100644
--- a/activerecord/test/cases/explain_test.rb
+++ b/activerecord/test/cases/explain_test.rb
@@ -2,7 +2,6 @@
require "cases/helper"
require "models/car"
-require "active_support/core_ext/string/strip"
if ActiveRecord::Base.connection.supports_explain?
class ExplainTest < ActiveRecord::TestCase
@@ -53,7 +52,7 @@ if ActiveRecord::Base.connection.supports_explain?
queries = sqls.zip(binds)
stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do
- expected = <<-SQL.strip_heredoc
+ expected = <<~SQL
EXPLAIN for: #{sqls[0]} [["wadus", 1]]
query plan foo
diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb
index 4039af66d0..59af4e6961 100644
--- a/activerecord/test/cases/finder_respond_to_test.rb
+++ b/activerecord/test/cases/finder_respond_to_test.rb
@@ -8,7 +8,7 @@ class FinderRespondToTest < ActiveRecord::TestCase
def test_should_preserve_normal_respond_to_behaviour_on_base
assert_respond_to ActiveRecord::Base, :new
- assert !ActiveRecord::Base.respond_to?(:find_by_something)
+ assert_not_respond_to ActiveRecord::Base, :find_by_something
end
def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method
@@ -43,14 +43,14 @@ class FinderRespondToTest < ActiveRecord::TestCase
end
def test_should_not_respond_to_find_by_one_missing_attribute
- assert !Topic.respond_to?(:find_by_undertitle)
+ assert_not_respond_to Topic, :find_by_undertitle
end
def test_should_not_respond_to_find_by_invalid_method_syntax
- assert !Topic.respond_to?(:fail_to_find_by_title)
- assert !Topic.respond_to?(:find_by_title?)
- assert !Topic.respond_to?(:fail_to_find_or_create_by_title)
- assert !Topic.respond_to?(:find_or_create_by_title?)
+ assert_not_respond_to Topic, :fail_to_find_by_title
+ assert_not_respond_to Topic, :find_by_title?
+ assert_not_respond_to Topic, :fail_to_find_or_create_by_title
+ assert_not_respond_to Topic, :find_or_create_by_title?
end
private
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 55496147c1..04150f4d57 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -9,6 +9,7 @@ require "models/company"
require "models/tagging"
require "models/topic"
require "models/reply"
+require "models/rating"
require "models/entrant"
require "models/project"
require "models/developer"
@@ -119,6 +120,21 @@ class FinderTest < ActiveRecord::TestCase
assert_equal "The Fourth Topic of the day", records[2].title
end
+ def test_find_with_ids_with_no_id_passed
+ exception = assert_raises(ActiveRecord::RecordNotFound) { Topic.find }
+ assert_equal exception.model, "Topic"
+ assert_equal exception.primary_key, "id"
+ end
+
+ def test_find_with_ids_with_id_out_of_range
+ exception = assert_raises(ActiveRecord::RecordNotFound) do
+ Topic.find("9999999999999999999999999999999")
+ end
+
+ assert_equal exception.model, "Topic"
+ assert_equal exception.primary_key, "id"
+ end
+
def test_find_passing_active_record_object_is_not_permitted
assert_raises(ArgumentError) do
Topic.find(Topic.last)
@@ -156,6 +172,32 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(NoMethodError) { Topic.exists?([1, 2]) }
end
+ def test_exists_with_scope
+ davids = Author.where(name: "David")
+ assert_equal true, davids.exists?
+ assert_equal true, davids.exists?(authors(:david).id)
+ assert_equal false, davids.exists?(authors(:mary).id)
+ assert_equal false, davids.exists?("42")
+ assert_equal false, davids.exists?(42)
+ assert_equal false, davids.exists?(davids.new.id)
+
+ fake = Author.where(name: "fake author")
+ assert_equal false, fake.exists?
+ assert_equal false, fake.exists?(authors(:david).id)
+ end
+
+ def test_exists_uses_existing_scope
+ post = authors(:david).posts.first
+ authors = Author.includes(:posts).where(name: "David", posts: { id: post.id })
+ assert_equal true, authors.exists?(authors(:david).id)
+ end
+
+ def test_any_with_scope_on_hash_includes
+ post = authors(:david).posts.first
+ categories = Categorization.includes(author: :posts).where(posts: { id: post.id })
+ assert_equal true, categories.exists?
+ end
+
def test_exists_with_polymorphic_relation
post = Post.create!(title: "Post", body: "default", taggings: [Tagging.new(comment: "tagging comment")])
relation = Post.tagged_with_comment("tagging comment")
@@ -212,7 +254,7 @@ class FinderTest < ActiveRecord::TestCase
# Ensure +exists?+ runs without an error by excluding order value.
def test_exists_with_order
- assert_equal true, Topic.order("invalid sql here").exists?
+ assert_equal true, Topic.order(Arel.sql("invalid sql here")).exists?
end
def test_exists_with_joins
@@ -228,20 +270,27 @@ class FinderTest < ActiveRecord::TestCase
end
def test_exists_with_includes_limit_and_empty_result
- assert_equal false, Topic.includes(:replies).limit(0).exists?
- assert_equal false, Topic.includes(:replies).limit(1).where("0 = 1").exists?
+ assert_no_queries { assert_equal false, Topic.includes(:replies).limit(0).exists? }
+ assert_queries(1) { assert_equal false, Topic.includes(:replies).limit(1).where("0 = 1").exists? }
end
def test_exists_with_distinct_association_includes_and_limit
author = Author.first
- assert_equal false, author.unique_categorized_posts.includes(:special_comments).limit(0).exists?
- assert_equal true, author.unique_categorized_posts.includes(:special_comments).limit(1).exists?
+ unique_categorized_posts = author.unique_categorized_posts.includes(:special_comments)
+ assert_no_queries { assert_equal false, unique_categorized_posts.limit(0).exists? }
+ assert_queries(1) { assert_equal true, unique_categorized_posts.limit(1).exists? }
end
def test_exists_with_distinct_association_includes_limit_and_order
author = Author.first
- assert_equal false, author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC").limit(0).exists?
- assert_equal true, author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC").limit(1).exists?
+ unique_categorized_posts = author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC")
+ assert_no_queries { assert_equal false, unique_categorized_posts.limit(0).exists? }
+ assert_queries(1) { assert_equal true, unique_categorized_posts.limit(1).exists? }
+ end
+
+ def test_exists_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association
+ ratings = developers(:david).ratings.includes(comment: :post).where(posts: { id: 1 })
+ assert_queries(1) { assert_not_predicate ratings.limit(1), :exists? }
end
def test_exists_with_empty_table_and_no_args_given
@@ -516,7 +565,7 @@ class FinderTest < ActiveRecord::TestCase
assert_nil Topic.offset(4).second_to_last
assert_nil Topic.offset(5).second_to_last
- #test with limit
+ # test with limit
assert_nil Topic.limit(1).second
assert_nil Topic.limit(1).second_to_last
end
@@ -599,13 +648,13 @@ class FinderTest < ActiveRecord::TestCase
def test_last_with_integer_and_order_should_use_sql_limit
relation = Topic.order("title")
assert_queries(1) { relation.last(5) }
- assert !relation.loaded?
+ assert_not_predicate 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?
+ assert_not_predicate relation, :loaded?
end
def test_last_on_loaded_relation_should_not_use_sql
@@ -618,7 +667,7 @@ class FinderTest < ActiveRecord::TestCase
def test_last_with_irreversible_order
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("coalesce(author_name, title)").last
+ Topic.order(Arel.sql("coalesce(author_name, title)")).last
end
end
@@ -630,12 +679,34 @@ class FinderTest < ActiveRecord::TestCase
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)
+ assert_equal comments.offset(2).to_a.last, comments.offset(2).last
+ assert_equal comments.offset(2).to_a.last(2), comments.offset(2).last(2)
+ assert_equal comments.offset(2).to_a.last(3), comments.offset(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_first_on_relation_with_limit_and_offset
+ post = posts("sti_comments")
+
+ comments = post.comments.order(id: :asc)
+ assert_equal comments.limit(2).to_a.first, comments.limit(2).first
+ assert_equal comments.limit(2).to_a.first(2), comments.limit(2).first(2)
+ assert_equal comments.limit(2).to_a.first(3), comments.limit(2).first(3)
+
+ assert_equal comments.offset(2).to_a.first, comments.offset(2).first
+ assert_equal comments.offset(2).to_a.first(2), comments.offset(2).first(2)
+ assert_equal comments.offset(2).to_a.first(3), comments.offset(2).first(3)
+
+ comments = comments.offset(1)
+ assert_equal comments.limit(2).to_a.first, comments.limit(2).first
+ assert_equal comments.limit(2).to_a.first(2), comments.limit(2).first(2)
+ assert_equal comments.limit(2).to_a.first(3), comments.limit(2).first(3)
+ end
+
def test_take_and_first_and_last_with_integer_should_return_an_array
assert_kind_of Array, Topic.take(5)
assert_kind_of Array, Topic.first(5)
@@ -656,8 +727,8 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ActiveModel::MissingAttributeError) { topic.title? }
assert_nil topic.read_attribute("title")
assert_equal "David", topic.author_name
- assert !topic.attribute_present?("title")
- assert !topic.attribute_present?(:title)
+ assert_not topic.attribute_present?("title")
+ assert_not topic.attribute_present?(:title)
assert topic.attribute_present?("author_name")
assert_respond_to topic, "author_name"
end
@@ -741,6 +812,15 @@ class FinderTest < ActiveRecord::TestCase
assert_equal [1, 2, 6, 7, 8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort
end
+ def test_find_on_hash_conditions_with_open_ended_range
+ assert_equal [1, 2, 3], Comment.where(id: Float::INFINITY..3).to_a.map(&:id).sort
+ end
+
+ def test_find_on_hash_conditions_with_numeric_range_for_string
+ topic = Topic.create!(title: "12 Factor App")
+ assert_equal [topic], Topic.where(title: 10..2).to_a
+ end
+
def test_find_on_multiple_hash_conditions
assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1)
assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) }
@@ -797,6 +877,25 @@ class FinderTest < ActiveRecord::TestCase
assert_equal customers(:david), found_customer
end
+ def test_hash_condition_find_with_aggregate_having_three_mappings_array
+ david_address = customers(:david).address
+ zaphod_address = customers(:zaphod).address
+ barney_address = customers(:barney).address
+ assert_kind_of Address, david_address
+ assert_kind_of Address, zaphod_address
+ found_customers = Customer.where(address: [david_address, zaphod_address, barney_address])
+ assert_equal [customers(:david), customers(:zaphod), customers(:barney)], found_customers.sort_by(&:id)
+ end
+
+ def test_hash_condition_find_with_aggregate_having_one_mapping_array
+ david_balance = customers(:david).balance
+ zaphod_balance = customers(:zaphod).balance
+ assert_kind_of Money, david_balance
+ assert_kind_of Money, zaphod_balance
+ found_customers = Customer.where(balance: [david_balance, zaphod_balance])
+ assert_equal [customers(:david), customers(:zaphod)], found_customers.sort_by(&:id)
+ end
+
def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate
gps_location = customers(:david).gps_location
assert_kind_of GpsLocation, gps_location
@@ -1002,14 +1101,6 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ArgumentError) { Topic.find_by_title_and_author_name("The First Topic") }
end
- def test_find_last_with_offset
- devs = Developer.order("id")
-
- assert_equal devs[2], Developer.offset(2).first
- assert_equal devs[-3], Developer.offset(2).last
- assert_equal devs[-3], Developer.offset(2).order("id DESC").first
- end
-
def test_find_by_nil_attribute
topic = Topic.find_by_last_read nil
assert_not_nil topic
@@ -1098,6 +1189,11 @@ class FinderTest < ActiveRecord::TestCase
order("author_addresses_authors.id DESC").limit(3).to_a.size
end
+ def test_find_with_eager_loading_collection_and_ordering_by_collection_primary_key
+ assert_equal Post.first, Post.eager_load(comments: :ratings).
+ order("posts.id, ratings.id, comments.id").first
+ end
+
def test_find_with_nil_inside_set_passed_for_one_attribute
client_of = Company.
where(client_of: [2, 1, nil],
@@ -1189,7 +1285,7 @@ class FinderTest < ActiveRecord::TestCase
test "find_by with associations" do
assert_equal authors(:david), Post.find_by(author: authors(:david)).author
- assert_equal authors(:mary) , Post.find_by(author: authors(:mary)).author
+ assert_equal authors(:mary), Post.find_by(author: authors(:mary)).author
end
test "find_by doesn't have implicit ordering" do
@@ -1254,12 +1350,12 @@ class FinderTest < ActiveRecord::TestCase
test "#skip_query_cache! for #exists? with a limited eager load" do
Topic.cache do
- assert_queries(2) do
+ assert_queries(1) do
Topic.eager_load(:replies).limit(1).exists?
Topic.eager_load(:replies).limit(1).exists?
end
- assert_queries(4) do
+ assert_queries(2) do
Topic.eager_load(:replies).limit(1).skip_query_cache!.exists?
Topic.eager_load(:replies).limit(1).skip_query_cache!.exists?
end
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index b0b63f5203..a4fa3c285b 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -79,6 +79,151 @@ class FixturesTest < ActiveRecord::TestCase
ActiveSupport::Notifications.unsubscribe(subscription)
end
end
+
+ def test_bulk_insert_multiple_table_with_a_multi_statement_query
+ subscriber = InsertQuerySubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
+
+ create_fixtures("bulbs", "authors", "computers")
+
+ expected_sql = <<~EOS.chop
+ INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("bulbs")} .*
+ INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("authors")} .*
+ INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("computers")} .*
+ EOS
+ assert_equal 1, subscriber.events.size
+ assert_match(/#{expected_sql}/, subscriber.events.first)
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+
+ def test_bulk_insert_with_a_multi_statement_query_raises_an_exception_when_any_insert_fails
+ require "models/aircraft"
+
+ assert_equal false, Aircraft.columns_hash["wheels_count"].null
+ fixtures = {
+ "aircraft" => [
+ { "name" => "working_aircrafts", "wheels_count" => 2 },
+ { "name" => "broken_aircrafts", "wheels_count" => nil },
+ ]
+ }
+
+ assert_no_difference "Aircraft.count" do
+ assert_raises(ActiveRecord::NotNullViolation) do
+ ActiveRecord::Base.connection.insert_fixtures_set(fixtures)
+ end
+ end
+ end
+ end
+
+ if current_adapter?(:Mysql2Adapter)
+ def test_insert_fixtures_set_raises_an_error_when_max_allowed_packet_is_smaller_than_fixtures_set_size
+ conn = ActiveRecord::Base.connection
+ mysql_margin = 2
+ packet_size = 1024
+ bytes_needed_to_have_a_1024_bytes_fixture = 858
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a" * bytes_needed_to_have_a_1024_bytes_fixture] },
+ ]
+ }
+
+ conn.stubs(:max_allowed_packet).returns(packet_size - mysql_margin)
+
+ error = assert_raises(ActiveRecord::ActiveRecordError) { conn.insert_fixtures_set(fixtures) }
+ assert_match(/Fixtures set is too large #{packet_size}\./, error.message)
+ end
+
+ def test_insert_fixture_set_when_max_allowed_packet_is_bigger_than_fixtures_set_size
+ conn = ActiveRecord::Base.connection
+ packet_size = 1024
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 51] },
+ ]
+ }
+
+ conn.stubs(:max_allowed_packet).returns(packet_size)
+
+ assert_difference "TrafficLight.count" do
+ conn.insert_fixtures_set(fixtures)
+ end
+ end
+
+ def test_insert_fixtures_set_split_the_total_sql_into_two_chunks_smaller_than_max_allowed_packet
+ subscriber = InsertQuerySubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
+ conn = ActiveRecord::Base.connection
+ packet_size = 1024
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 450] },
+ ],
+ "comments" => [
+ { "post_id" => 1, "body" => "a" * 450 },
+ ]
+ }
+
+ conn.stubs(:max_allowed_packet).returns(packet_size)
+
+ conn.insert_fixtures_set(fixtures)
+ assert_equal 2, subscriber.events.size
+ assert_operator subscriber.events.first.bytesize, :<, packet_size
+ assert_operator subscriber.events.second.bytesize, :<, packet_size
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+
+ def test_insert_fixtures_set_concat_total_sql_into_a_single_packet_smaller_than_max_allowed_packet
+ subscriber = InsertQuerySubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
+ conn = ActiveRecord::Base.connection
+ packet_size = 1024
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 200] },
+ ],
+ "comments" => [
+ { "post_id" => 1, "body" => "a" * 200 },
+ ]
+ }
+
+ conn.stubs(:max_allowed_packet).returns(packet_size)
+
+ assert_difference ["TrafficLight.count", "Comment.count"], +1 do
+ conn.insert_fixtures_set(fixtures)
+ end
+ assert_equal 1, subscriber.events.size
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+ end
+
+ def test_auto_value_on_primary_key
+ fixtures = [
+ { "name" => "first", "wheels_count" => 2 },
+ { "name" => "second", "wheels_count" => 3 }
+ ]
+ conn = ActiveRecord::Base.connection
+ assert_nothing_raised do
+ conn.insert_fixtures_set({ "aircraft" => fixtures }, ["aircraft"])
+ end
+ result = conn.select_all("SELECT name, wheels_count FROM aircraft ORDER BY id")
+ assert_equal fixtures, result.to_a
+ end
+
+ def test_deprecated_insert_fixtures
+ fixtures = [
+ { "name" => "first", "wheels_count" => 2 },
+ { "name" => "second", "wheels_count" => 3 }
+ ]
+ conn = ActiveRecord::Base.connection
+ conn.delete("DELETE FROM aircraft")
+ assert_deprecated do
+ conn.insert_fixtures(fixtures, "aircraft")
+ end
+ result = conn.select_all("SELECT name, wheels_count FROM aircraft ORDER BY id")
+ assert_equal fixtures, result.to_a
end
def test_broken_yaml_exception
@@ -247,8 +392,8 @@ class FixturesTest < ActiveRecord::TestCase
def test_nonexistent_fixture_file
nonexistent_fixture_path = FIXTURES_ROOT + "/imnothere"
- #sanity check to make sure that this file never exists
- assert Dir[nonexistent_fixture_path + "*"].empty?
+ # sanity check to make sure that this file never exists
+ assert_empty Dir[nonexistent_fixture_path + "*"]
assert_raise(Errno::ENOENT) do
ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, nonexistent_fixture_path)
@@ -447,10 +592,10 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase
fixtures :topics, :developers, :accounts
def test_without_complete_instantiation
- assert !defined?(@first)
- assert !defined?(@topics)
- assert !defined?(@developers)
- assert !defined?(@accounts)
+ assert_not defined?(@first)
+ assert_not defined?(@topics)
+ assert_not defined?(@developers)
+ assert_not defined?(@accounts)
end
def test_fixtures_from_root_yml_without_instantiation
@@ -937,13 +1082,13 @@ class FoxyFixturesTest < ActiveRecord::TestCase
def test_supports_inline_habtm
assert(parrots(:george).treasures.include?(treasures(:diamond)))
assert(parrots(:george).treasures.include?(treasures(:sapphire)))
- assert(!parrots(:george).treasures.include?(treasures(:ruby)))
+ assert_not(parrots(:george).treasures.include?(treasures(:ruby)))
end
def test_supports_inline_habtm_with_specified_id
assert(parrots(:polly).treasures.include?(treasures(:ruby)))
assert(parrots(:polly).treasures.include?(treasures(:sapphire)))
- assert(!parrots(:polly).treasures.include?(treasures(:diamond)))
+ assert_not(parrots(:polly).treasures.include?(treasures(:diamond)))
end
def test_supports_yaml_arrays
@@ -998,10 +1143,10 @@ class FoxyFixturesTest < ActiveRecord::TestCase
end
def test_resolves_enums
- assert books(:awdr).published?
- assert books(:awdr).read?
- assert books(:rfr).proposed?
- assert books(:ddd).published?
+ assert_predicate books(:awdr), :published?
+ assert_predicate books(:awdr), :read?
+ assert_predicate books(:rfr), :proposed?
+ assert_predicate books(:ddd), :published?
end
end
@@ -1041,7 +1186,7 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase
end
def test_table_name_is_defined_in_the_model
- assert_equal "randomly_named_table2", ActiveRecord::FixtureSet::all_loaded_fixtures["admin/randomly_named_a9"].table_name
+ assert_equal "randomly_named_table2", ActiveRecord::FixtureSet.all_loaded_fixtures["admin/randomly_named_a9"].table_name
assert_equal "randomly_named_table2", Admin::ClassNameThatDoesNotFollowCONVENTIONS1.table_name
end
end
diff --git a/activerecord/test/cases/habtm_destroy_order_test.rb b/activerecord/test/cases/habtm_destroy_order_test.rb
index 5e503272e1..b15e1b48c4 100644
--- a/activerecord/test/cases/habtm_destroy_order_test.rb
+++ b/activerecord/test/cases/habtm_destroy_order_test.rb
@@ -15,7 +15,7 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase
sicp.destroy
end
end
- assert !sicp.destroyed?
+ assert_not_predicate sicp, :destroyed?
end
test "should not raise error if have foreign key in the join table" do
@@ -42,7 +42,7 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase
ben.lessons << sicp
ben.save!
ben.destroy
- assert !ben.reload.lessons.empty?
+ assert_not_empty ben.reload.lessons
ensure
# get rid of it so Student is still like it was
Student.reset_callbacks(:destroy)
@@ -58,6 +58,6 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase
assert_raises LessonError do
sicp.destroy
end
- assert !sicp.reload.students.empty?
+ assert_not_empty sicp.reload.students
end
end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 6ea02ac191..66f11fe5bd 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -184,4 +184,4 @@ module InTimeZone
end
end
-require "mocha/setup" # FIXME: stop using mocha
+require "mocha/minitest" # FIXME: stop using mocha
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index c931f7d21c..7a5c06b894 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -130,61 +130,70 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_descends_from_active_record
- assert !ActiveRecord::Base.descends_from_active_record?
+ assert_not_predicate ActiveRecord::Base, :descends_from_active_record?
# Abstract subclass of AR::Base.
- assert LoosePerson.descends_from_active_record?
+ assert_predicate LoosePerson, :descends_from_active_record?
# Concrete subclass of an abstract class.
- assert LooseDescendant.descends_from_active_record?
+ assert_predicate LooseDescendant, :descends_from_active_record?
# Concrete subclass of AR::Base.
- assert TightPerson.descends_from_active_record?
+ assert_predicate TightPerson, :descends_from_active_record?
# Concrete subclass of a concrete class but has no type column.
- assert TightDescendant.descends_from_active_record?
+ assert_predicate TightDescendant, :descends_from_active_record?
# Concrete subclass of AR::Base.
- assert Post.descends_from_active_record?
+ assert_predicate Post, :descends_from_active_record?
# Concrete subclasses of a concrete class which has a type column.
- assert !StiPost.descends_from_active_record?
- assert !SubStiPost.descends_from_active_record?
+ assert_not_predicate StiPost, :descends_from_active_record?
+ assert_not_predicate SubStiPost, :descends_from_active_record?
# Abstract subclass of a concrete class which has a type column.
# This is pathological, as you'll never have Sub < Abstract < Concrete.
- assert !AbstractStiPost.descends_from_active_record?
+ assert_not_predicate AbstractStiPost, :descends_from_active_record?
# Concrete subclass of an abstract class which has a type column.
- assert !SubAbstractStiPost.descends_from_active_record?
+ assert_not_predicate SubAbstractStiPost, :descends_from_active_record?
end
def test_company_descends_from_active_record
- assert !ActiveRecord::Base.descends_from_active_record?
+ assert_not_predicate ActiveRecord::Base, :descends_from_active_record?
assert AbstractCompany.descends_from_active_record?, "AbstractCompany should descend from ActiveRecord::Base"
assert Company.descends_from_active_record?, "Company should descend from ActiveRecord::Base"
assert !Class.new(Company).descends_from_active_record?, "Company subclass should not descend from ActiveRecord::Base"
end
def test_abstract_class
- assert !ActiveRecord::Base.abstract_class?
- assert LoosePerson.abstract_class?
- assert !LooseDescendant.abstract_class?
+ assert_not_predicate ActiveRecord::Base, :abstract_class?
+ assert_predicate LoosePerson, :abstract_class?
+ assert_not_predicate LooseDescendant, :abstract_class?
end
def test_inheritance_base_class
assert_equal Post, Post.base_class
+ assert_predicate Post, :base_class?
assert_equal Post, SpecialPost.base_class
+ assert_not_predicate SpecialPost, :base_class?
assert_equal Post, StiPost.base_class
+ assert_not_predicate StiPost, :base_class?
assert_equal Post, SubStiPost.base_class
+ assert_not_predicate SubStiPost, :base_class?
assert_equal SubAbstractStiPost, SubAbstractStiPost.base_class
+ assert_predicate SubAbstractStiPost, :base_class?
end
def test_abstract_inheritance_base_class
assert_equal LoosePerson, LoosePerson.base_class
+ assert_predicate LoosePerson, :base_class?
assert_equal LooseDescendant, LooseDescendant.base_class
+ assert_predicate LooseDescendant, :base_class?
assert_equal TightPerson, TightPerson.base_class
+ assert_predicate TightPerson, :base_class?
assert_equal TightPerson, TightDescendant.base_class
+ assert_not_predicate TightDescendant, :base_class?
end
def test_base_class_activerecord_error
@@ -280,6 +289,21 @@ class InheritanceTest < ActiveRecord::TestCase
assert_equal Firm, firm.class
end
+ def test_where_new_with_subclass
+ firm = Company.where(type: "Firm").new
+ assert_equal Firm, firm.class
+ end
+
+ def test_where_create_with_subclass
+ firm = Company.where(type: "Firm").create(name: "Basecamp")
+ assert_equal Firm, firm.class
+ end
+
+ def test_where_create_bang_with_subclass
+ firm = Company.where(type: "Firm").create!(name: "Basecamp")
+ assert_equal Firm, firm.class
+ end
+
def test_new_with_abstract_class
e = assert_raises(NotImplementedError) do
AbstractCompany.new
@@ -302,6 +326,30 @@ class InheritanceTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::SubclassNotFound) { Company.new(type: "Account") }
end
+ def test_where_new_with_invalid_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").new }
+ end
+
+ def test_where_new_with_unrelated_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").new }
+ end
+
+ def test_where_create_with_invalid_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").create }
+ end
+
+ def test_where_create_with_unrelated_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").create }
+ end
+
+ def test_where_create_bang_with_invalid_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").create! }
+ end
+
+ def test_where_create_bang_with_unrelated_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").create! }
+ end
+
def test_new_with_unrelated_namespaced_type
without_store_full_sti_class do
e = assert_raises ActiveRecord::SubclassNotFound do
diff --git a/activerecord/test/cases/instrumentation_test.rb b/activerecord/test/cases/instrumentation_test.rb
new file mode 100644
index 0000000000..e6e8468757
--- /dev/null
+++ b/activerecord/test/cases/instrumentation_test.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/book"
+
+module ActiveRecord
+ class InstrumentationTest < ActiveRecord::TestCase
+ def test_payload_name_on_load
+ Book.create(name: "test book")
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "SELECT"
+ assert_equal "Book Load", event.payload[:name]
+ end
+ end
+ Book.first
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ def test_payload_name_on_create
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "INSERT"
+ assert_equal "Book Create", event.payload[:name]
+ end
+ end
+ Book.create(name: "test book")
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ def test_payload_name_on_update
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "UPDATE"
+ assert_equal "Book Update", event.payload[:name]
+ end
+ end
+ book = Book.create(name: "test book")
+ book.update_attribute(:name, "new name")
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ def test_payload_name_on_update_all
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "UPDATE"
+ assert_equal "Book Update All", event.payload[:name]
+ end
+ end
+ Book.create(name: "test book")
+ Book.update_all(name: "new name")
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ def test_payload_name_on_destroy
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "DELETE"
+ assert_equal "Book Destroy", event.payload[:name]
+ end
+ end
+ book = Book.create(name: "test book")
+ book.destroy
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+ end
+end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index 20e747142b..9c85543b9b 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -161,6 +161,15 @@ module ActiveRecord
end
end
+ class UpOnlyMigration < SilentMigration
+ def change
+ add_column :horses, :oldie, :integer, default: 0
+ up_only { execute "update horses set oldie = 1" }
+ end
+ end
+
+ self.use_transactional_tests = false
+
setup do
@verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, false
end
@@ -206,7 +215,7 @@ module ActiveRecord
migration = InvertibleMigration.new
migration.migrate :up
migration.migrate :down
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
end
def test_migrate_revert
@@ -214,11 +223,11 @@ module ActiveRecord
revert = InvertibleRevertMigration.new
migration.migrate :up
revert.migrate :up
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
revert.migrate :down
assert migration.connection.table_exists?("horses")
migration.migrate :down
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
end
def test_migrate_revert_by_part
@@ -232,12 +241,12 @@ module ActiveRecord
}
migration.migrate :up
assert_equal [:both, :up], received
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
assert migration.connection.table_exists?("new_horses")
migration.migrate :down
assert_equal [:both, :up, :both, :down], received
assert migration.connection.table_exists?("horses")
- assert !migration.connection.table_exists?("new_horses")
+ assert_not migration.connection.table_exists?("new_horses")
end
def test_migrate_revert_whole_migration
@@ -246,11 +255,11 @@ module ActiveRecord
revert = RevertWholeMigration.new(klass)
migration.migrate :up
revert.migrate :up
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
revert.migrate :down
assert migration.connection.table_exists?("horses")
migration.migrate :down
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
end
end
@@ -259,7 +268,7 @@ module ActiveRecord
revert.migrate :down
assert revert.connection.table_exists?("horses")
revert.migrate :up
- assert !revert.connection.table_exists?("horses")
+ assert_not revert.connection.table_exists?("horses")
end
def test_migrate_revert_change_column_default
@@ -295,6 +304,8 @@ module ActiveRecord
migration2.migrate(:down)
assert_equal false, Horse.connection.extension_enabled?("hstore")
+ ensure
+ enable_extension!("hstore", ActiveRecord::Base.connection)
end
end
@@ -376,5 +387,23 @@ module ActiveRecord
"horses_index_named index should not exist"
end
end
+
+ def test_up_only
+ InvertibleMigration.new.migrate(:up)
+ horse1 = Horse.create
+ # populates existing horses with oldie = 1 but new ones have default 0
+ UpOnlyMigration.new.migrate(:up)
+ Horse.reset_column_information
+ horse1.reload
+ horse2 = Horse.create
+
+ assert 1, horse1.oldie # created before migration
+ assert 0, horse2.oldie # created after migration
+
+ UpOnlyMigration.new.migrate(:down) # should be no error
+ connection = ActiveRecord::Base.connection
+ assert_not connection.column_exists?(:horses, :oldie)
+ Horse.reset_column_information
+ end
end
end
diff --git a/activerecord/test/cases/json_attribute_test.rb b/activerecord/test/cases/json_attribute_test.rb
index 63f3c77fc3..afc39d0420 100644
--- a/activerecord/test/cases/json_attribute_test.rb
+++ b/activerecord/test/cases/json_attribute_test.rb
@@ -19,14 +19,14 @@ class JsonAttributeTest < ActiveRecord::TestCase
def setup
super
@connection.create_table("json_data_type") do |t|
- t.text "payload"
- t.text "settings"
+ t.string "payload"
+ t.string "settings"
end
end
private
def column_type
- :text
+ :string
end
def klass
diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb
index 52fe488cd5..82cf281cff 100644
--- a/activerecord/test/cases/json_serialization_test.rb
+++ b/activerecord/test/cases/json_serialization_test.rb
@@ -252,7 +252,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
def @david.favorite_quote; "Constraints are liberating"; end
json = @david.to_json(include: :posts, methods: :favorite_quote)
- assert !@david.posts.first.respond_to?(:favorite_quote)
+ assert_not_respond_to @david.posts.first, :favorite_quote
assert_match %r{"favorite_quote":"Constraints are liberating"}, json
assert_equal 1, %r{"favorite_quote":}.match(json).size
end
diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb
index 56ec8c8a82..9b79803503 100644
--- a/activerecord/test/cases/json_shared_test_cases.rb
+++ b/activerecord/test/cases/json_shared_test_cases.rb
@@ -23,10 +23,10 @@ module JSONSharedTestCases
def test_column
column = klass.columns_hash["payload"]
assert_equal column_type, column.type
- assert_equal column_type.to_s, column.sql_type
+ assert_type_match column_type, column.sql_type
type = klass.type_for_attribute("payload")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_change_table_supports_json
@@ -36,7 +36,7 @@ module JSONSharedTestCases
klass.reset_column_information
column = klass.columns_hash["users"]
assert_equal column_type, column.type
- assert_equal column_type.to_s, column.sql_type
+ assert_type_match column_type, column.sql_type
end
def test_schema_dumping
@@ -66,26 +66,26 @@ module JSONSharedTestCases
end
def test_rewrite
- @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|)
+ @connection.execute(insert_statement_per_database('{"k":"v"}'))
x = klass.first
x.payload = { '"a\'' => "b" }
assert x.save!
end
def test_select
- @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|)
+ @connection.execute(insert_statement_per_database('{"k":"v"}'))
x = klass.first
assert_equal({ "k" => "v" }, x.payload)
end
def test_select_multikey
- @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')|)
+ @connection.execute(insert_statement_per_database('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}'))
x = klass.first
assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload)
end
def test_null_json
- @connection.execute("insert into json_data_type (payload) VALUES(null)")
+ @connection.execute(insert_statement_per_database("null"))
x = klass.first
assert_nil(x.payload)
end
@@ -101,19 +101,19 @@ module JSONSharedTestCases
x = klass.where(payload: nil).first
assert_nil(x)
- json.update_attributes(payload: nil)
+ json.update(payload: nil)
x = klass.where(payload: nil).first
assert_equal(json.reload, x)
end
def test_select_array_json_value
- @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|)
+ @connection.execute(insert_statement_per_database('["v0",{"k1":"v1"}]'))
x = klass.first
assert_equal(["v0", { "k1" => "v1" }], x.payload)
end
def test_rewrite_array_json_value
- @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|)
+ @connection.execute(insert_statement_per_database('["v0",{"k1":"v1"}]'))
x = klass.first
x.payload = ["v1", { "k2" => "v2" }, "v3"]
assert x.save!
@@ -152,42 +152,42 @@ module JSONSharedTestCases
def test_changes_in_place
json = klass.new
- assert_not json.changed?
+ assert_not_predicate json, :changed?
json.payload = { "one" => "two" }
- assert json.changed?
- assert json.payload_changed?
+ assert_predicate json, :changed?
+ assert_predicate json, :payload_changed?
json.save!
- assert_not json.changed?
+ assert_not_predicate json, :changed?
json.payload["three"] = "four"
- assert json.payload_changed?
+ assert_predicate json, :payload_changed?
json.save!
json.reload
assert_equal({ "one" => "two", "three" => "four" }, json.payload)
- assert_not json.changed?
+ assert_not_predicate json, :changed?
end
def test_changes_in_place_ignores_key_order
json = klass.new
- assert_not json.changed?
+ assert_not_predicate json, :changed?
json.payload = { "three" => "four", "one" => "two" }
json.save!
json.reload
json.payload = { "three" => "four", "one" => "two" }
- assert_not json.changed?
+ assert_not_predicate json, :changed?
json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }]
json.save!
json.reload
json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }]
- assert_not json.changed?
+ assert_not_predicate json, :changed?
end
def test_changes_in_place_with_ruby_object
@@ -195,10 +195,10 @@ module JSONSharedTestCases
json = klass.create!(payload: time)
json.reload
- assert_not json.changed?
+ assert_not_predicate json, :changed?
json.payload = time
- assert_not json.changed?
+ assert_not_predicate json, :changed?
end
def test_assigning_string_literal
@@ -253,4 +253,17 @@ module JSONSharedTestCases
def klass
JsonDataType
end
+
+ def assert_type_match(type, sql_type)
+ native_type = ActiveRecord::Base.connection.native_database_types[type][:name]
+ assert_match %r(\A#{native_type}\b), sql_type
+ end
+
+ def insert_statement_per_database(values)
+ if current_adapter?(:OracleAdapter)
+ "insert into json_data_type (id, payload) VALUES (json_data_type_seq.nextval, '#{values}')"
+ else
+ "insert into json_data_type (payload) VALUES ('#{values}')"
+ end
+ end
end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 743680ba92..8513edb0ab 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -15,6 +15,7 @@ require "models/bulb"
require "models/engine"
require "models/wheel"
require "models/treasure"
+require "models/frog"
class LockWithoutDefault < ActiveRecord::Base; end
@@ -69,8 +70,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::StaleObjectError) { s2.destroy }
assert s1.destroy
- assert s1.frozen?
- assert s1.destroyed?
+ assert_predicate s1, :frozen?
+ assert_predicate s1, :destroyed?
assert_raises(ActiveRecord::RecordNotFound) { StringKeyObject.find("record1") }
end
@@ -104,8 +105,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::StaleObjectError) { p2.destroy }
assert p1.destroy
- assert p1.frozen?
- assert p1.destroyed?
+ assert_predicate p1, :frozen?
+ assert_predicate p1, :destroyed?
assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) }
end
@@ -194,6 +195,45 @@ class OptimisticLockingTest < ActiveRecord::TestCase
end
end
+ def test_update_with_dirty_primary_key
+ assert_raises(ActiveRecord::RecordNotUnique) do
+ person = Person.find(1)
+ person.id = 2
+ person.save!
+ end
+
+ person = Person.find(1)
+ person.id = 42
+ person.save!
+
+ assert Person.find(42)
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Person.find(1)
+ end
+ end
+
+ def test_delete_with_dirty_primary_key
+ person = Person.find(1)
+ person.id = 2
+ person.delete
+
+ assert Person.find(2)
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Person.find(1)
+ end
+ end
+
+ def test_destroy_with_dirty_primary_key
+ person = Person.find(1)
+ person.id = 2
+ person.destroy
+
+ assert Person.find(2)
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Person.find(1)
+ end
+ end
+
def test_explicit_update_lock_column_raise_error
person = Person.find(1)
@@ -201,7 +241,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
person.first_name = "Douglas Adams"
person.lock_version = 42
- assert person.lock_version_changed?
+ assert_predicate person, :lock_version_changed?
person.save
end
@@ -399,25 +439,59 @@ class OptimisticLockingTest < ActiveRecord::TestCase
end
end
+ def test_counter_cache_with_touch_and_lock_version
+ car = Car.create!
+
+ assert_equal 0, car.wheels_count
+ assert_equal 0, car.lock_version
+
+ previously_car_updated_at = car.updated_at
+ travel(2.second) do
+ Wheel.create!(wheelable: car)
+ end
+
+ assert_equal 1, car.reload.wheels_count
+ assert_not_equal previously_car_updated_at, car.updated_at
+ assert_equal 1, car.lock_version
+
+ previously_car_updated_at = car.updated_at
+ travel(1.day) do
+ car.wheels.first.update(size: 42)
+ end
+
+ assert_equal 1, car.reload.wheels_count
+ assert_not_equal previously_car_updated_at, car.updated_at
+ assert_equal 2, car.lock_version
+
+ previously_car_updated_at = car.updated_at
+ travel(2.second) do
+ car.wheels.first.destroy!
+ end
+
+ assert_equal 0, car.reload.wheels_count
+ assert_not_equal previously_car_updated_at, car.updated_at
+ assert_equal 3, car.lock_version
+ end
+
def test_polymorphic_destroy_with_dependencies_and_lock_version
car = Car.create!
assert_difference "car.wheels.count" do
- car.wheels << Wheel.create!
+ car.wheels.create
end
assert_difference "car.wheels.count", -1 do
car.reload.destroy
end
- assert car.destroyed?
+ assert_predicate car, :destroyed?
end
def test_removing_has_and_belongs_to_many_associations_upon_destroy
p = RichPerson.create! first_name: "Jon"
p.treasures.create!
- assert !p.treasures.empty?
+ assert_not_empty p.treasures
p.destroy
- assert p.treasures.empty?
- assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty?
+ assert_empty p.treasures
+ assert_empty RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1")
end
def test_yaml_dumping_with_lock_column
@@ -487,7 +561,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
t1.destroy
- assert t1.destroyed?
+ assert_predicate t1, :destroyed?
end
def test_destroy_stale_object
@@ -500,7 +574,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
stale_object.destroy!
end
- refute stale_object.destroyed?
+ assert_not_predicate stale_object, :destroyed?
end
private
@@ -565,17 +639,27 @@ unless in_memory_db?
end
end
- # Locking a record reloads it.
- def test_sane_lock_method
+ def test_lock_does_not_raise_when_the_object_is_not_dirty
+ person = Person.find 1
assert_nothing_raised do
- Person.transaction do
- person = Person.find 1
- old, person.first_name = person.first_name, "fooman"
- # Locking a dirty record is deprecated
- assert_deprecated do
- person.lock!
- end
- assert_equal old, person.first_name
+ person.lock!
+ end
+ end
+
+ def test_lock_raises_when_the_record_is_dirty
+ person = Person.find 1
+ person.first_name = "fooman"
+ assert_raises(RuntimeError) do
+ person.lock!
+ end
+ end
+
+ def test_locking_in_after_save_callback
+ assert_nothing_raised do
+ frog = ::Frog.create(name: "Old Frog")
+ frog.name = "New Frog"
+ assert_not_deprecated do
+ frog.save!
end
end
end
@@ -611,14 +695,12 @@ unless in_memory_db?
end
end
- if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
- def test_no_locks_no_wait
- first, second = duel { Person.find 1 }
- assert first.end > second.end
- end
-
- private
+ def test_no_locks_no_wait
+ first, second = duel { Person.find 1 }
+ assert first.end > second.end
+ end
+ private
def duel(zzz = 5)
t0, t1, t2, t3 = nil, nil, nil, nil
@@ -646,6 +728,5 @@ unless in_memory_db?
assert t3 > t2
[t0.to_f..t1.to_f, t2.to_f..t3.to_f]
end
- end
end
end
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index 208e54ed0b..e2742ed33e 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -33,8 +33,9 @@ class LogSubscriberTest < ActiveRecord::TestCase
super
end
- def debug(message)
- @debugs << message
+ def debug(progname = nil, &block)
+ @debugs << progname
+ super
end
end
@@ -171,6 +172,22 @@ class LogSubscriberTest < ActiveRecord::TestCase
assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last)
end
+ def test_vebose_query_logs
+ ActiveRecord::Base.verbose_query_logs = true
+
+ logger = TestDebugLogSubscriber.new
+ logger.sql(Event.new(0, sql: "hi mom!"))
+ assert_match(/↳/, @logger.logged(:debug).last)
+ ensure
+ ActiveRecord::Base.verbose_query_logs = false
+ end
+
+ def test_verbose_query_logs_disabled_by_default
+ logger = TestDebugLogSubscriber.new
+ logger.sql(Event.new(0, sql: "hi mom!"))
+ assert_no_match(/↳/, @logger.logged(:debug).last)
+ end
+
def test_cached_queries
ActiveRecord::Base.cache do
Developer.all.load
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index 7b0644e9c0..f4d16cb093 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -84,7 +84,7 @@ module ActiveRecord
columns = connection.columns(:testings)
array_column = columns.detect { |c| c.name == "foo" }
- assert array_column.array?
+ assert_predicate array_column, :array?
end
def test_create_table_with_array_column
@@ -95,7 +95,7 @@ module ActiveRecord
columns = connection.columns(:testings)
array_column = columns.detect { |c| c.name == "foo" }
- assert array_column.array?
+ assert_predicate array_column, :array?
end
end
@@ -205,8 +205,8 @@ module ActiveRecord
created_at_column = created_columns.detect { |c| c.name == "created_at" }
updated_at_column = created_columns.detect { |c| c.name == "updated_at" }
- assert !created_at_column.null
- assert !updated_at_column.null
+ assert_not created_at_column.null
+ assert_not updated_at_column.null
end
def test_create_table_with_timestamps_should_create_datetime_columns_with_options
@@ -264,19 +264,18 @@ module ActiveRecord
t.column :foo, :timestamp
end
- klass = Class.new(ActiveRecord::Base)
- klass.table_name = "testings"
+ column = connection.columns(:testings).find { |c| c.name == "foo" }
- assert_equal :datetime, klass.columns_hash["foo"].type
+ assert_equal :datetime, column.type
if current_adapter?(:PostgreSQLAdapter)
- assert_equal "timestamp without time zone", klass.columns_hash["foo"].sql_type
+ assert_equal "timestamp without time zone", column.sql_type
elsif current_adapter?(:Mysql2Adapter)
- assert_equal "timestamp", klass.columns_hash["foo"].sql_type
+ assert_equal "timestamp", column.sql_type
elsif current_adapter?(:OracleAdapter)
- assert_equal "TIMESTAMP(6)", klass.columns_hash["foo"].sql_type
+ assert_equal "TIMESTAMP(6)", column.sql_type
else
- assert_equal klass.connection.type_to_sql("datetime"), klass.columns_hash["foo"].sql_type
+ assert_equal connection.type_to_sql("datetime"), column.sql_type
end
end
@@ -409,7 +408,7 @@ module ActiveRecord
end
connection.change_table :testings do |t|
assert t.column_exists?(:foo)
- assert !(t.column_exists?(:bar))
+ assert_not (t.column_exists?(:bar))
end
end
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index be6dc2acb1..3022121f4c 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -80,7 +80,7 @@ module ActiveRecord
TestModel.delete_all
# Now use the Rails insertion
- TestModel.create wealth: BigDecimal.new("12345678901234567890.0123456789")
+ TestModel.create wealth: BigDecimal("12345678901234567890.0123456789")
# SELECT
row = TestModel.first
@@ -146,7 +146,7 @@ module ActiveRecord
TestModel.create first_name: "bob", last_name: "bobsen",
bio: "I was born ....", age: 18, height: 1.78,
- wealth: BigDecimal.new("12345678901234567890.0123456789"),
+ wealth: BigDecimal("12345678901234567890.0123456789"),
birthday: 18.years.ago, favorite_day: 10.days.ago,
moment_of_truth: "1782-10-10 21:40:18", male: true
@@ -159,7 +159,7 @@ module ActiveRecord
# Test for 30 significant digits (beyond the 16 of float), 10 of them
# after the decimal place.
- assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth
+ assert_equal BigDecimal("0012345678901234567890.0123456789"), bob.wealth
assert_equal true, bob.male?
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index 8ca20b6172..cedd9c44e3 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -67,7 +67,7 @@ module ActiveRecord
if current_adapter?(:Mysql2Adapter)
def test_mysql_rename_column_preserves_auto_increment
rename_column "test_models", "id", "id_test"
- assert connection.columns("test_models").find { |c| c.name == "id_test" }.auto_increment?
+ assert_predicate connection.columns("test_models").find { |c| c.name == "id_test" }, :auto_increment?
TestModel.reset_column_information
ensure
rename_column "test_models", "id_test", "id"
@@ -223,31 +223,31 @@ module ActiveRecord
def test_change_column_with_nil_default
add_column "test_models", "contributor", :boolean, default: true
- assert TestModel.new.contributor?
+ assert_predicate TestModel.new, :contributor?
change_column "test_models", "contributor", :boolean, default: nil
TestModel.reset_column_information
- assert_not TestModel.new.contributor?
+ assert_not_predicate TestModel.new, :contributor?
assert_nil TestModel.new.contributor
end
def test_change_column_to_drop_default_with_null_false
add_column "test_models", "contributor", :boolean, default: true, null: false
- assert TestModel.new.contributor?
+ assert_predicate TestModel.new, :contributor?
change_column "test_models", "contributor", :boolean, default: nil, null: false
TestModel.reset_column_information
- assert_not TestModel.new.contributor?
+ assert_not_predicate TestModel.new, :contributor?
assert_nil TestModel.new.contributor
end
def test_change_column_with_new_default
add_column "test_models", "administrator", :boolean, default: true
- assert TestModel.new.administrator?
+ assert_predicate TestModel.new, :administrator?
change_column "test_models", "administrator", :boolean, default: false
TestModel.reset_column_information
- assert_not TestModel.new.administrator?
+ assert_not_predicate TestModel.new, :administrator?
end
def test_change_column_with_custom_index_name
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index 0b5e983f14..3a11bb081b 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -14,7 +14,7 @@ module ActiveRecord
recorder = CommandRecorder.new(Class.new {
def america; end
}.new)
- assert recorder.respond_to?(:america)
+ assert_respond_to recorder, :america
end
def test_send_calls_super
@@ -27,7 +27,7 @@ module ActiveRecord
recorder = CommandRecorder.new(Class.new {
def create_table(name); end
}.new)
- assert recorder.respond_to?(:create_table), "respond_to? create_table"
+ assert_respond_to recorder, :create_table
recorder.send(:create_table, :horses)
assert_equal [[:create_table, [:horses], nil]], recorder.commands
end
@@ -213,6 +213,11 @@ module ActiveRecord
assert_equal [:remove_index, [:table, { name: "new_index" }]], remove
end
+ def test_invert_add_index_with_algorithm_option
+ remove = @recorder.inverse_of :add_index, [:table, :one, algorithm: :concurrently]
+ assert_equal [:remove_index, [:table, { column: :one, algorithm: :concurrently }]], remove
+ end
+
def test_invert_remove_index
add = @recorder.inverse_of :remove_index, [:table, :one]
assert_equal [:add_index, [:table, :one]], add
diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb
index cb3b02c02a..69a50674af 100644
--- a/activerecord/test/cases/migration/compatibility_test.rb
+++ b/activerecord/test/cases/migration/compatibility_test.rb
@@ -16,7 +16,7 @@ module ActiveRecord
ActiveRecord::Migration.verbose = false
connection.create_table :testings do |t|
- t.column :foo, :string, limit: 100
+ t.column :foo, :string, limit: 5
t.column :bar, :string, limit: 100
end
end
@@ -126,15 +126,32 @@ module ActiveRecord
end
assert_match(/LegacyMigration < ActiveRecord::Migration\[4\.2\]/, e.message)
end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ class Testing < ActiveRecord::Base
+ end
+
+ def test_legacy_change_column_with_null_executes_update
+ migration = Class.new(ActiveRecord::Migration[5.1]) {
+ def migrate(x)
+ change_column :testings, :foo, :string, limit: 10, null: false, default: "foobar"
+ end
+ }.new
+
+ Testing.create!
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+ assert_equal ["foobar"], Testing.all.map(&:foo)
+ ensure
+ ActiveRecord::Base.clear_cache!
+ end
+ end
end
end
end
-class LegacyPrimaryKeyTest < ActiveRecord::TestCase
+module LegacyPrimaryKeyTestCases
include SchemaDumpingHelper
- self.use_transactional_tests = false
-
class LegacyPrimaryKey < ActiveRecord::Base
end
@@ -152,7 +169,7 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase
end
def test_legacy_primary_key_should_be_auto_incremented
- @migration = Class.new(ActiveRecord::Migration[5.0]) {
+ @migration = Class.new(migration_class) {
def change
create_table :legacy_primary_keys do |t|
t.references :legacy_ref
@@ -162,12 +179,10 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase
@migration.migrate(:up)
- legacy_pk = LegacyPrimaryKey.columns_hash["id"]
- assert_not legacy_pk.bigint?
- assert_not legacy_pk.null
+ assert_legacy_primary_key
legacy_ref = LegacyPrimaryKey.columns_hash["legacy_ref_id"]
- assert_not legacy_ref.bigint?
+ assert_not_predicate legacy_ref, :bigint?
record1 = LegacyPrimaryKey.create!
assert_not_nil record1.id
@@ -182,7 +197,7 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase
def test_legacy_integer_primary_key_should_not_be_auto_incremented
skip if current_adapter?(:SQLite3Adapter)
- @migration = Class.new(ActiveRecord::Migration[5.0]) {
+ @migration = Class.new(migration_class) {
def change
create_table :legacy_primary_keys, id: :integer do |t|
end
@@ -199,9 +214,85 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase
assert_match %r{create_table "legacy_primary_keys", id: :integer, default: nil}, schema
end
+ def test_legacy_primary_key_in_create_table_should_be_integer
+ @migration = Class.new(migration_class) {
+ def change
+ create_table :legacy_primary_keys, id: false do |t|
+ t.primary_key :id
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ assert_legacy_primary_key
+ end
+
+ def test_legacy_primary_key_in_change_table_should_be_integer
+ @migration = Class.new(migration_class) {
+ def change
+ create_table :legacy_primary_keys, id: false do |t|
+ t.integer :dummy
+ end
+ change_table :legacy_primary_keys do |t|
+ t.primary_key :id
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ assert_legacy_primary_key
+ end
+
+ def test_add_column_with_legacy_primary_key_should_be_integer
+ @migration = Class.new(migration_class) {
+ def change
+ create_table :legacy_primary_keys, id: false do |t|
+ t.integer :dummy
+ end
+ add_column :legacy_primary_keys, :id, :primary_key
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ assert_legacy_primary_key
+ end
+
+ def test_legacy_join_table_foreign_keys_should_be_integer
+ @migration = Class.new(migration_class) {
+ def change
+ create_join_table :apples, :bananas do |t|
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ schema = dump_table_schema "apples_bananas"
+ assert_match %r{integer "apple_id", null: false}, schema
+ assert_match %r{integer "banana_id", null: false}, schema
+ end
+
+ def test_legacy_join_table_column_options_should_be_overwritten
+ @migration = Class.new(migration_class) {
+ def change
+ create_join_table :apples, :bananas, column_options: { type: :bigint } do |t|
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ schema = dump_table_schema "apples_bananas"
+ assert_match %r{bigint "apple_id", null: false}, schema
+ assert_match %r{bigint "banana_id", null: false}, schema
+ end
+
if current_adapter?(:Mysql2Adapter)
def test_legacy_bigint_primary_key_should_be_auto_incremented
- @migration = Class.new(ActiveRecord::Migration[5.0]) {
+ @migration = Class.new(migration_class) {
def change
create_table :legacy_primary_keys, id: :bigint
end
@@ -210,15 +301,15 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase
@migration.migrate(:up)
legacy_pk = LegacyPrimaryKey.columns_hash["id"]
- assert legacy_pk.bigint?
- assert legacy_pk.auto_increment?
+ assert_predicate legacy_pk, :bigint?
+ assert_predicate legacy_pk, :auto_increment?
schema = dump_table_schema "legacy_primary_keys"
assert_match %r{create_table "legacy_primary_keys", (?!id: :bigint, default: nil)}, schema
end
else
def test_legacy_bigint_primary_key_should_not_be_auto_incremented
- @migration = Class.new(ActiveRecord::Migration[5.0]) {
+ @migration = Class.new(migration_class) {
def change
create_table :legacy_primary_keys, id: :bigint do |t|
end
@@ -235,4 +326,44 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase
assert_match %r{create_table "legacy_primary_keys", id: :bigint, default: nil}, schema
end
end
+
+ private
+ def assert_legacy_primary_key
+ assert_equal "id", LegacyPrimaryKey.primary_key
+
+ legacy_pk = LegacyPrimaryKey.columns_hash["id"]
+
+ assert_equal :integer, legacy_pk.type
+ assert_not_predicate legacy_pk, :bigint?
+ assert_not legacy_pk.null
+
+ if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
+ schema = dump_table_schema "legacy_primary_keys"
+ assert_match %r{create_table "legacy_primary_keys", id: :(?:integer|serial), (?!default: nil)}, schema
+ end
+ end
+end
+
+module LegacyPrimaryKeyTest
+ class V5_0 < ActiveRecord::TestCase
+ include LegacyPrimaryKeyTestCases
+
+ self.use_transactional_tests = false
+
+ private
+ def migration_class
+ ActiveRecord::Migration[5.0]
+ end
+ end
+
+ class V4_2 < ActiveRecord::TestCase
+ include LegacyPrimaryKeyTestCases
+
+ self.use_transactional_tests = false
+
+ private
+ def migration_class
+ ActiveRecord::Migration[4.2]
+ end
+ end
end
diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb
index 77d32a24a5..a1e5fb1115 100644
--- a/activerecord/test/cases/migration/create_join_table_test.rb
+++ b/activerecord/test/cases/migration/create_join_table_test.rb
@@ -69,7 +69,7 @@ module ActiveRecord
def test_create_join_table_without_indexes
connection.create_join_table :artists, :musics
- assert connection.indexes(:artists_musics).blank?
+ assert_predicate connection.indexes(:artists_musics), :blank?
end
def test_create_join_table_with_index
@@ -95,42 +95,42 @@ module ActiveRecord
connection.create_join_table :artists, :musics
connection.drop_join_table :artists, :musics
- assert !connection.table_exists?("artists_musics")
+ assert_not connection.table_exists?("artists_musics")
end
def test_drop_join_table_with_strings
connection.create_join_table :artists, :musics
connection.drop_join_table "artists", "musics"
- assert !connection.table_exists?("artists_musics")
+ assert_not connection.table_exists?("artists_musics")
end
def test_drop_join_table_with_the_proper_order
connection.create_join_table :videos, :musics
connection.drop_join_table :videos, :musics
- assert !connection.table_exists?("musics_videos")
+ assert_not connection.table_exists?("musics_videos")
end
def test_drop_join_table_with_the_table_name
connection.create_join_table :artists, :musics, table_name: :catalog
connection.drop_join_table :artists, :musics, table_name: :catalog
- assert !connection.table_exists?("catalog")
+ assert_not connection.table_exists?("catalog")
end
def test_drop_join_table_with_the_table_name_as_string
connection.create_join_table :artists, :musics, table_name: "catalog"
connection.drop_join_table :artists, :musics, table_name: "catalog"
- assert !connection.table_exists?("catalog")
+ assert_not connection.table_exists?("catalog")
end
def test_drop_join_table_with_column_options
connection.create_join_table :artists, :musics, column_options: { null: true }
connection.drop_join_table :artists, :musics, column_options: { null: true }
- assert !connection.table_exists?("artists_musics")
+ assert_not connection.table_exists?("artists_musics")
end
def test_create_and_drop_join_table_with_common_prefix
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
index 499d072de5..50f5696ad1 100644
--- a/activerecord/test/cases/migration/foreign_key_test.rb
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -227,6 +227,74 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
end
+ if ActiveRecord::Base.connection.supports_validate_constraints?
+ def test_add_invalid_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_not_predicate fk, :validated?
+ end
+
+ def test_validate_foreign_key_infers_column
+ @connection.add_foreign_key :astronauts, :rockets, validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, :rockets
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_key_by_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, column: "rocket_id"
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_key_by_symbol_column
+ @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id, validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, column: :rocket_id
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_key_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, name: "fancy_named_fk"
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_non_existing_foreign_key_raises
+ assert_raises ArgumentError do
+ @connection.validate_foreign_key :astronauts, :rockets
+ end
+ end
+
+ def test_validate_constraint_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false
+
+ @connection.validate_constraint :astronauts, "fancy_named_fk"
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+ else
+ # Foreign key should still be created, but should not be invalid
+ def test_add_invalid_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_predicate fk, :validated?
+ end
+ end
+
def test_schema_dumping
@connection.add_foreign_key :astronauts, :rockets
output = dump_table_schema "astronauts"
@@ -238,6 +306,17 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
end
+ def test_schema_dumping_with_custom_fk_ignore_pattern
+ original_pattern = ActiveRecord::SchemaDumper.fk_ignore_pattern
+ ActiveRecord::SchemaDumper.fk_ignore_pattern = /^ignored_/
+ @connection.add_foreign_key :astronauts, :rockets, name: :ignored_fk_astronauts_rockets
+
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output
+
+ ActiveRecord::SchemaDumper.fk_ignore_pattern = original_pattern
+ end
+
def test_schema_dumping_on_delete_and_on_update_options
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index bf1ebdb4c5..f9b2dc0c73 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -33,10 +33,8 @@ module ActiveRecord
connection.add_index(table_name, [:foo], name: "old_idx")
connection.rename_index(table_name, "old_idx", "new_idx")
- assert_deprecated do
- assert_not connection.index_name_exists?(table_name, "old_idx", false)
- assert connection.index_name_exists?(table_name, "new_idx", true)
- end
+ assert_not connection.index_name_exists?(table_name, "old_idx")
+ assert connection.index_name_exists?(table_name, "new_idx")
end
def test_rename_index_too_long
@@ -101,7 +99,7 @@ module ActiveRecord
connection.add_index :testings, :foo
assert connection.index_exists?(:testings, :foo)
- assert !connection.index_exists?(:testings, :bar)
+ assert_not connection.index_exists?(:testings, :bar)
end
def test_index_exists_on_multiple_columns
@@ -133,7 +131,7 @@ module ActiveRecord
assert connection.index_exists?(:testings, :foo)
assert connection.index_exists?(:testings, :foo, name: "custom_index_name")
- assert !connection.index_exists?(:testings, :foo, name: "other_index_name")
+ assert_not connection.index_exists?(:testings, :foo, name: "other_index_name")
end
def test_remove_named_index
@@ -141,7 +139,7 @@ module ActiveRecord
assert connection.index_exists?(:testings, :foo)
connection.remove_index :testings, :foo
- assert !connection.index_exists?(:testings, :foo)
+ assert_not connection.index_exists?(:testings, :foo)
end
def test_add_index_attribute_length_limit
@@ -205,7 +203,7 @@ module ActiveRecord
assert connection.index_exists?("testings", "last_name")
connection.remove_index("testings", "last_name")
- assert !connection.index_exists?("testings", "last_name")
+ assert_not connection.index_exists?("testings", "last_name")
end
end
diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb
index d0066f68be..dedb5ea502 100644
--- a/activerecord/test/cases/migration/pending_migrations_test.rb
+++ b/activerecord/test/cases/migration/pending_migrations_test.rb
@@ -4,37 +4,37 @@ require "cases/helper"
module ActiveRecord
class Migration
- class PendingMigrationsTest < ActiveRecord::TestCase
- def setup
- super
- @connection = Minitest::Mock.new
- @app = Minitest::Mock.new
- conn = @connection
- @pending = Class.new(CheckPending) {
- define_method(:connection) { conn }
- }.new(@app)
- @pending.instance_variable_set :@last_check, -1 # Force checking
- end
+ if current_adapter?(:SQLite3Adapter) && !in_memory_db?
+ class PendingMigrationsTest < ActiveRecord::TestCase
+ setup do
+ file = ActiveRecord::Base.connection.raw_connection.filename
+ @conn = ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:", migrations_paths: MIGRATIONS_ROOT + "/valid"
+ source_db = SQLite3::Database.new file
+ dest_db = ActiveRecord::Base.connection.raw_connection
+ backup = SQLite3::Backup.new(dest_db, "main", source_db, "main")
+ backup.step(-1)
+ backup.finish
+ end
- def teardown
- assert @connection.verify
- assert @app.verify
- super
- end
+ teardown do
+ @conn.release_connection if @conn
+ ActiveRecord::Base.establish_connection :arunit
+ end
+
+ def test_errors_if_pending
+ ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
- def test_errors_if_pending
- ActiveRecord::Migrator.stub :needs_migration?, true do
- assert_raise ActiveRecord::PendingMigrationError do
- @pending.call(nil)
+ assert_raises ActiveRecord::PendingMigrationError do
+ CheckPending.new(Proc.new {}).call({})
end
end
- end
- def test_checks_if_supported
- @app.expect :call, nil, [:foo]
+ def test_checks_if_supported
+ ActiveRecord::SchemaMigration.create_table
+ migrator = Base.connection.migration_context
+ capture(:stdout) { migrator.migrate }
- ActiveRecord::Migrator.stub :needs_migration?, false do
- @pending.call(:foo)
+ assert_nil CheckPending.new(Proc.new {}).call({})
end
end
end
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index dfce266253..a9deb92585 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -86,9 +86,9 @@ module ActiveRecord
rename_table :cats, :felines
assert connection.table_exists? :felines
- refute connection.table_exists? :cats
+ assert_not connection.table_exists? :cats
- primary_key_name = connection.select_values(<<-SQL.strip_heredoc, "SCHEMA")[0]
+ primary_key_name = connection.select_values(<<~SQL, "SCHEMA")[0]
SELECT c.relname
FROM pg_class c
JOIN pg_index i
@@ -107,7 +107,7 @@ module ActiveRecord
connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()"
assert_nothing_raised { rename_table :cats, :felines }
assert connection.table_exists? :felines
- refute connection.table_exists? :cats
+ assert_not connection.table_exists? :cats
ensure
connection.drop_table :cats, if_exists: true
connection.drop_table :felines, if_exists: true
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 07afa89779..1fa9a3c34a 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -71,6 +71,16 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Migration.verbose = @verbose_was
end
+ def test_migrator_migrations_path_is_deprecated
+ assert_deprecated do
+ ActiveRecord::Migrator.migrations_path = "/whatever"
+ end
+ ensure
+ assert_deprecated do
+ ActiveRecord::Migrator.migrations_path = "db/migrate"
+ end
+ end
+
def test_migration_version_matches_component_version
assert_equal ActiveRecord::VERSION::STRING.to_f, ActiveRecord::Migration.current_version
end
@@ -78,20 +88,20 @@ class MigrationTest < ActiveRecord::TestCase
def test_migrator_versions
migrations_path = MIGRATIONS_ROOT + "/valid"
old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = migrations_path
+ migrator = ActiveRecord::MigrationContext.new(migrations_path)
- ActiveRecord::Migrator.up(migrations_path)
- assert_equal 3, ActiveRecord::Migrator.current_version
- assert_equal false, ActiveRecord::Migrator.needs_migration?
+ migrator.up
+ assert_equal 3, migrator.current_version
+ assert_equal false, migrator.needs_migration?
- ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid")
- assert_equal 0, ActiveRecord::Migrator.current_version
- assert_equal true, ActiveRecord::Migrator.needs_migration?
+ migrator.down
+ assert_equal 0, migrator.current_version
+ assert_equal true, migrator.needs_migration?
ActiveRecord::SchemaMigration.create!(version: 3)
- assert_equal true, ActiveRecord::Migrator.needs_migration?
+ assert_equal true, migrator.needs_migration?
ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ ActiveRecord::MigrationContext.new(old_path)
end
def test_migration_detection_without_schema_migration_table
@@ -99,28 +109,31 @@ class MigrationTest < ActiveRecord::TestCase
migrations_path = MIGRATIONS_ROOT + "/valid"
old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = migrations_path
+ migrator = ActiveRecord::MigrationContext.new(migrations_path)
- assert_equal true, ActiveRecord::Migrator.needs_migration?
+ assert_equal true, migrator.needs_migration?
ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ ActiveRecord::MigrationContext.new(old_path)
end
def test_any_migrations
old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/valid"
+ migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid")
- assert ActiveRecord::Migrator.any_migrations?
+ assert_predicate migrator, :any_migrations?
- ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/empty"
+ migrator_empty = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/empty")
- assert_not ActiveRecord::Migrator.any_migrations?
+ assert_not_predicate migrator_empty, :any_migrations?
ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ ActiveRecord::MigrationContext.new(old_path)
end
def test_migration_version
- assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) }
+ migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/version_check")
+ assert_equal 0, migrator.current_version
+ migrator.up(20131219224947)
+ assert_equal 20131219224947, migrator.current_version
end
def test_create_table_with_force_true_does_not_drop_nonexisting_table
@@ -157,14 +170,14 @@ class MigrationTest < ActiveRecord::TestCase
def test_add_table_with_decimals
Person.connection.drop_table :big_numbers rescue nil
- assert !BigNumber.table_exists?
+ assert_not_predicate BigNumber, :table_exists?
GiveMeBigNumbers.up
BigNumber.reset_column_information
assert BigNumber.create(
bank_balance: 1586.43,
big_bank_balance: BigDecimal("1000234000567.95"),
- world_population: 6000000000,
+ world_population: 2**62,
my_house_population: 3,
value_of_e: BigDecimal("2.7182818284590452353602875")
)
@@ -178,10 +191,8 @@ class MigrationTest < ActiveRecord::TestCase
assert_not_nil b.my_house_population
assert_not_nil b.value_of_e
- # TODO: set world_population >= 2**62 to cover 64-bit platforms and test
- # is_a?(Bignum)
assert_kind_of Integer, b.world_population
- assert_equal 6000000000, b.world_population
+ assert_equal 2**62, b.world_population
assert_kind_of Integer, b.my_house_population
assert_equal 3, b.my_house_population
assert_kind_of BigDecimal, b.bank_balance
@@ -216,15 +227,16 @@ class MigrationTest < ActiveRecord::TestCase
def test_filtering_migrations
assert_no_column Person, :last_name
- assert !Reminder.table_exists?
+ assert_not_predicate Reminder, :table_exists?
name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" }
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter)
+ migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid")
+ migrator.up(&name_filter)
assert_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
- ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter)
+ migrator.down(&name_filter)
assert_no_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
@@ -382,9 +394,9 @@ class MigrationTest < ActiveRecord::TestCase
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
migrations_path = MIGRATIONS_ROOT + "/valid"
old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = migrations_path
+ migrator = ActiveRecord::MigrationContext.new(migrations_path)
- ActiveRecord::Migrator.up(migrations_path)
+ migrator.up
assert_equal current_env, ActiveRecord::InternalMetadata[:environment]
original_rails_env = ENV["RAILS_ENV"]
@@ -392,16 +404,16 @@ class MigrationTest < ActiveRecord::TestCase
ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo"
new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
- refute_equal current_env, new_env
+ assert_not_equal current_env, new_env
sleep 1 # mysql by default does not store fractional seconds in the database
- ActiveRecord::Migrator.up(migrations_path)
+ migrator.up
assert_equal new_env, ActiveRecord::InternalMetadata[:environment]
ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ migrator = ActiveRecord::MigrationContext.new(old_path)
ENV["RAILS_ENV"] = original_rails_env
ENV["RACK_ENV"] = original_rack_env
- ActiveRecord::Migrator.up(migrations_path)
+ migrator.up
end
def test_internal_metadata_stores_environment_when_other_data_exists
@@ -411,14 +423,15 @@ class MigrationTest < ActiveRecord::TestCase
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
migrations_path = MIGRATIONS_ROOT + "/valid"
old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = migrations_path
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
- ActiveRecord::Migrator.up(migrations_path)
+ migrator = ActiveRecord::MigrationContext.new(migrations_path)
+ migrator.up
assert_equal current_env, ActiveRecord::InternalMetadata[:environment]
assert_equal "bar", ActiveRecord::InternalMetadata[:foo]
ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ migrator = ActiveRecord::MigrationContext.new(old_path)
+ migrator.up
end
def test_proper_table_name_on_migration
@@ -450,7 +463,7 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_rename_table_with_prefix_and_suffix
- assert !Thing.table_exists?
+ assert_not_predicate Thing, :table_exists?
ActiveRecord::Base.table_name_prefix = "p_"
ActiveRecord::Base.table_name_suffix = "_s"
Thing.reset_table_name
@@ -471,7 +484,7 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_add_drop_table_with_prefix_and_suffix
- assert !Reminder.table_exists?
+ assert_not_predicate Reminder, :table_exists?
ActiveRecord::Base.table_name_prefix = "prefix_"
ActiveRecord::Base.table_name_suffix = "_suffix"
Reminder.reset_table_name
@@ -535,7 +548,7 @@ class MigrationTest < ActiveRecord::TestCase
end
assert Person.connection.column_exists?(:something, :foo)
assert_nothing_raised { Person.connection.remove_column :something, :foo, :bar }
- assert !Person.connection.column_exists?(:something, :foo)
+ assert_not Person.connection.column_exists?(:something, :foo)
assert Person.connection.column_exists?(:something, :name)
assert Person.connection.column_exists?(:something, :number)
ensure
@@ -678,6 +691,25 @@ class MigrationTest < ActiveRecord::TestCase
assert_no_column Person, :last_name,
"without an advisory lock, the Migrator should not make any changes, but it did."
end
+
+ def test_with_advisory_lock_raises_the_right_error_when_it_fails_to_release_lock
+ migration = Class.new(ActiveRecord::Migration::Current).new
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+ lock_id = migrator.send(:generate_migrator_advisory_lock_id)
+
+ e = assert_raises(ActiveRecord::ConcurrentMigrationError) do
+ silence_stream($stderr) do
+ migrator.send(:with_advisory_lock) do
+ ActiveRecord::Base.connection.release_advisory_lock(lock_id)
+ end
+ end
+ end
+
+ assert_match(
+ /#{ActiveRecord::ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE}/,
+ e.message
+ )
+ end
end
private
@@ -790,7 +822,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
end
end
- [:qualification, :experience].each { |c| assert ! column(c) }
+ [:qualification, :experience].each { |c| assert_not column(c) }
assert column(:qualification_experience)
end
@@ -801,8 +833,15 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
t.integer :age
end
- # Adding an index fires a query every time to check if an index already exists or not
- assert_queries(3) do
+ classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
+ expected_query_count = {
+ "Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not
+ "PostgreSQLAdapter" => 2,
+ }.fetch(classname) {
+ raise "need an expected query count for #{classname}"
+ }
+
+ assert_queries(expected_query_count) do
with_bulk_change_table do |t|
t.index :username, unique: true, name: :awesome_username_index
t.index [:name, :age]
@@ -813,7 +852,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
name_age_index = index(:index_delete_me_on_name_and_age)
assert_equal ["name", "age"].sort, name_age_index.columns.sort
- assert ! name_age_index.unique
+ assert_not name_age_index.unique
assert index(:awesome_username_index).unique
end
@@ -826,14 +865,22 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
assert index(:index_delete_me_on_name)
- assert_queries(3) do
+ classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
+ expected_query_count = {
+ "Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not
+ "PostgreSQLAdapter" => 2,
+ }.fetch(classname) {
+ raise "need an expected query count for #{classname}"
+ }
+
+ assert_queries(expected_query_count) do
with_bulk_change_table do |t|
t.remove_index :name
t.index :name, name: :new_name_index, unique: true
end
end
- assert ! index(:index_delete_me_on_name)
+ assert_not index(:index_delete_me_on_name)
new_name_index = index(:new_name_index)
assert new_name_index.unique
@@ -845,21 +892,27 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
t.date :birthdate
end
- assert ! column(:name).default
+ assert_not column(:name).default
assert_equal :date, column(:birthdate).type
- # One query for columns (delete_me table)
- # One query for primary key (delete_me table)
- # One query to do the bulk change
- assert_queries(3, ignore_none: true) do
+ classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
+ expected_query_count = {
+ "Mysql2Adapter" => 3, # one query for columns, one query for primary key, one query to do the bulk change
+ "PostgreSQLAdapter" => 3, # one query for columns, one for bulk change, one for comment
+ }.fetch(classname) {
+ raise "need an expected query count for #{classname}"
+ }
+
+ assert_queries(expected_query_count, ignore_none: true) do
with_bulk_change_table do |t|
t.change :name, :string, default: "NONAME"
- t.change :birthdate, :datetime
+ t.change :birthdate, :datetime, comment: "This is a comment"
end
end
assert_equal "NONAME", column(:name).default
assert_equal :datetime, column(:birthdate).type
+ assert_equal "This is a comment", column(:birthdate).comment
end
private
@@ -919,7 +972,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
files_count = Dir[@migrations_path + "/*.rb"].length
copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy")
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
+ assert_empty copied
ensure
clear
end
@@ -960,7 +1013,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
files_count = Dir[@migrations_path + "/*.rb"].length
copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps")
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
+ assert_empty copied
end
ensure
clear
@@ -1002,7 +1055,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
files_count = Dir[@migrations_path + "/*.rb"].length
copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps")
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
+ assert_empty copied
end
ensure
clear
@@ -1023,7 +1076,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
files_count = Dir[@migrations_path + "/*.rb"].length
copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/magic")
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
+ assert_empty copied
ensure
clear
end
@@ -1106,17 +1159,4 @@ class CopyMigrationsTest < ActiveRecord::TestCase
def test_unknown_migration_version_should_raise_an_argument_error
assert_raise(ArgumentError) { ActiveRecord::Migration[1.0] }
end
-
- def test_deprecate_initialize_internal_tables
- assert_deprecated { ActiveRecord::Base.connection.initialize_schema_migrations_table }
- assert_deprecated { ActiveRecord::Base.connection.initialize_internal_metadata_table }
- end
-
- def test_deprecate_supports_migrations
- assert_deprecated { ActiveRecord::Base.connection.supports_migrations? }
- end
-
- def test_deprecate_schema_migrations_table_name
- assert_deprecated { ActiveRecord::Migrator.schema_migrations_table_name }
- end
end
diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb
index ee10be119c..873455cf67 100644
--- a/activerecord/test/cases/migrator_test.rb
+++ b/activerecord/test/cases/migrator_test.rb
@@ -66,10 +66,30 @@ class MigratorTest < ActiveRecord::TestCase
list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
ActiveRecord::Migrator.new(:up, list, 3).run
end
+
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
+ ActiveRecord::Migrator.new(:up, list, -1).run
+ end
+
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
+ ActiveRecord::Migrator.new(:up, list, 0).run
+ end
+
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
+ ActiveRecord::Migrator.new(:up, list, 3).migrate
+ end
+
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
+ ActiveRecord::Migrator.new(:up, list, -1).migrate
+ end
end
def test_finds_migrations
- migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid")
+ migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid").migrations
[[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i|
assert_equal migrations[i].version, pair.first
@@ -78,7 +98,8 @@ class MigratorTest < ActiveRecord::TestCase
end
def test_finds_migrations_in_subdirectories
- migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_subdirectories")
+ migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid_with_subdirectories").migrations
+
[[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i|
assert_equal migrations[i].version, pair.first
@@ -88,7 +109,7 @@ class MigratorTest < ActiveRecord::TestCase
def test_finds_migrations_from_two_directories
directories = [MIGRATIONS_ROOT + "/valid_with_timestamps", MIGRATIONS_ROOT + "/to_copy_with_timestamps"]
- migrations = ActiveRecord::Migrator.migrations directories
+ migrations = ActiveRecord::MigrationContext.new(directories).migrations
[[20090101010101, "PeopleHaveHobbies"],
[20090101010202, "PeopleHaveDescriptions"],
@@ -101,14 +122,14 @@ class MigratorTest < ActiveRecord::TestCase
end
def test_finds_migrations_in_numbered_directory
- migrations = ActiveRecord::Migrator.migrations [MIGRATIONS_ROOT + "/10_urban"]
+ migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/10_urban").migrations
assert_equal 9, migrations[0].version
assert_equal "AddExpressions", migrations[0].name
end
def test_relative_migrations
list = Dir.chdir(MIGRATIONS_ROOT) do
- ActiveRecord::Migrator.migrations("valid")
+ ActiveRecord::MigrationContext.new("valid").migrations
end
migration_proxy = list.find { |item|
@@ -137,7 +158,7 @@ class MigratorTest < ActiveRecord::TestCase
["up", "002", "We need reminders"],
["down", "003", "Innocent jointable"],
["up", "010", "********** NO FILE **********"],
- ], ActiveRecord::Migrator.migrations_status(path)
+ ], ActiveRecord::MigrationContext.new(path).migrations_status
end
def test_migrations_status_in_subdirectories
@@ -151,7 +172,7 @@ class MigratorTest < ActiveRecord::TestCase
["up", "002", "We need reminders"],
["down", "003", "Innocent jointable"],
["up", "010", "********** NO FILE **********"],
- ], ActiveRecord::Migrator.migrations_status(path)
+ ], ActiveRecord::MigrationContext.new(path).migrations_status
end
def test_migrations_status_with_schema_define_in_subdirectories
@@ -166,7 +187,7 @@ class MigratorTest < ActiveRecord::TestCase
["up", "001", "Valid people have last names"],
["up", "002", "We need reminders"],
["up", "003", "Innocent jointable"],
- ], ActiveRecord::Migrator.migrations_status(path)
+ ], ActiveRecord::MigrationContext.new(path).migrations_status
ensure
ActiveRecord::Migrator.migrations_paths = prev_paths
end
@@ -184,7 +205,7 @@ class MigratorTest < ActiveRecord::TestCase
["down", "20100201010101", "Valid with timestamps we need reminders"],
["down", "20100301010101", "Valid with timestamps innocent jointable"],
["up", "20160528010101", "********** NO FILE **********"],
- ], ActiveRecord::Migrator.migrations_status(paths)
+ ], ActiveRecord::MigrationContext.new(paths).migrations_status
end
def test_migrator_interleaved_migrations
@@ -212,25 +233,28 @@ class MigratorTest < ActiveRecord::TestCase
def test_up_calls_up
migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
- ActiveRecord::Migrator.new(:up, migrations).migrate
+ migrator = ActiveRecord::Migrator.new(:up, migrations)
+ migrator.migrate
assert migrations.all?(&:went_up)
assert migrations.all? { |m| !m.went_down }
- assert_equal 2, ActiveRecord::Migrator.current_version
+ assert_equal 2, migrator.current_version
end
def test_down_calls_down
test_up_calls_up
migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
- ActiveRecord::Migrator.new(:down, migrations).migrate
+ migrator = ActiveRecord::Migrator.new(:down, migrations)
+ migrator.migrate
assert migrations.all? { |m| !m.went_up }
assert migrations.all?(&:went_down)
- assert_equal 0, ActiveRecord::Migrator.current_version
+ assert_equal 0, migrator.current_version
end
def test_current_version
ActiveRecord::SchemaMigration.create!(version: "1000")
- assert_equal 1000, ActiveRecord::Migrator.current_version
+ migrator = ActiveRecord::MigrationContext.new("db/migrate")
+ assert_equal 1000, migrator.current_version
end
def test_migrator_one_up
@@ -269,33 +293,36 @@ class MigratorTest < ActiveRecord::TestCase
def test_migrator_double_up
calls, migrations = sensors(3)
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ migrator = ActiveRecord::Migrator.new(:up, migrations, 1)
+ assert_equal(0, migrator.current_version)
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ migrator.migrate
assert_equal [[:up, 1]], calls
calls.clear
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ migrator.migrate
assert_equal [], calls
end
def test_migrator_double_down
calls, migrations = sensors(3)
+ migrator = ActiveRecord::Migrator.new(:up, migrations, 1)
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ assert_equal 0, migrator.current_version
- ActiveRecord::Migrator.new(:up, migrations, 1).run
+ migrator.run
assert_equal [[:up, 1]], calls
calls.clear
- ActiveRecord::Migrator.new(:down, migrations, 1).run
+ migrator = ActiveRecord::Migrator.new(:down, migrations, 1)
+ migrator.run
assert_equal [[:down, 1]], calls
calls.clear
- ActiveRecord::Migrator.new(:down, migrations, 1).run
+ migrator.run
assert_equal [], calls
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ assert_equal 0, migrator.current_version
end
def test_migrator_verbosity
@@ -341,78 +368,85 @@ class MigratorTest < ActiveRecord::TestCase
def test_migrator_going_down_due_to_version_target
calls, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
- migrator.up("valid", 1)
+ migrator.up(1)
assert_equal [[:up, 1]], calls
calls.clear
- migrator.migrate("valid", 0)
+ migrator.migrate(0)
assert_equal [[:down, 1]], calls
calls.clear
- migrator.migrate("valid")
+ migrator.migrate
assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
end
def test_migrator_output_when_running_multiple_migrations
_, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
- result = migrator.migrate("valid")
+ result = migrator.migrate
assert_equal(3, result.count)
# Nothing migrated from duplicate run
- result = migrator.migrate("valid")
+ result = migrator.migrate
assert_equal(0, result.count)
- result = migrator.rollback("valid")
+ result = migrator.rollback
assert_equal(1, result.count)
end
def test_migrator_output_when_running_single_migration
_, migrator = migrator_class(1)
- result = migrator.run(:up, "valid", 1)
+ migrator = migrator.new("valid")
+
+ result = migrator.run(:up, 1)
assert_equal(1, result.version)
end
def test_migrator_rollback
_, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
- migrator.migrate("valid")
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ migrator.migrate
+ assert_equal(3, migrator.current_version)
- migrator.rollback("valid")
- assert_equal(2, ActiveRecord::Migrator.current_version)
+ migrator.rollback
+ assert_equal(2, migrator.current_version)
- migrator.rollback("valid")
- assert_equal(1, ActiveRecord::Migrator.current_version)
+ migrator.rollback
+ assert_equal(1, migrator.current_version)
- migrator.rollback("valid")
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ migrator.rollback
+ assert_equal(0, migrator.current_version)
- migrator.rollback("valid")
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ migrator.rollback
+ assert_equal(0, migrator.current_version)
end
def test_migrator_db_has_no_schema_migrations_table
_, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
assert_not ActiveRecord::Base.connection.table_exists?("schema_migrations")
- migrator.migrate("valid", 1)
+ migrator.migrate(1)
assert ActiveRecord::Base.connection.table_exists?("schema_migrations")
end
def test_migrator_forward
_, migrator = migrator_class(3)
- migrator.migrate("/valid", 1)
- assert_equal(1, ActiveRecord::Migrator.current_version)
+ migrator = migrator.new("/valid")
+ migrator.migrate(1)
+ assert_equal(1, migrator.current_version)
- migrator.forward("/valid", 2)
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ migrator.forward(2)
+ assert_equal(3, migrator.current_version)
- migrator.forward("/valid")
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ migrator.forward
+ assert_equal(3, migrator.current_version)
end
def test_only_loads_pending_migrations
@@ -420,25 +454,27 @@ class MigratorTest < ActiveRecord::TestCase
ActiveRecord::SchemaMigration.create!(version: "1")
calls, migrator = migrator_class(3)
- migrator.migrate("valid", nil)
+ migrator = migrator.new("valid")
+ migrator.migrate
assert_equal [[:up, 2], [:up, 3]], calls
end
def test_get_all_versions
_, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
- migrator.migrate("valid")
- assert_equal([1, 2, 3], ActiveRecord::Migrator.get_all_versions)
+ migrator.migrate
+ assert_equal([1, 2, 3], migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([1, 2], ActiveRecord::Migrator.get_all_versions)
+ migrator.rollback
+ assert_equal([1, 2], migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([1], ActiveRecord::Migrator.get_all_versions)
+ migrator.rollback
+ assert_equal([1], migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([], ActiveRecord::Migrator.get_all_versions)
+ migrator.rollback
+ assert_equal([], migrator.get_all_versions)
end
private
@@ -463,11 +499,11 @@ class MigratorTest < ActiveRecord::TestCase
def migrator_class(count)
calls, migrations = sensors(count)
- migrator = Class.new(ActiveRecord::Migrator).extend(Module.new {
- define_method(:migrations) { |paths|
+ migrator = Class.new(ActiveRecord::MigrationContext) {
+ define_method(:migrations) { |*|
migrations
}
- })
+ }
[calls, migrator]
end
end
diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb
index 59be4dc5a8..6f3903eed4 100644
--- a/activerecord/test/cases/multiparameter_attributes_test.rb
+++ b/activerecord/test/cases/multiparameter_attributes_test.rb
@@ -227,7 +227,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
- assert_equal false, topic.written_on.respond_to?(:time_zone)
+ assert_not_respond_to topic.written_on, :time_zone
end
end
@@ -242,7 +242,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
- assert_equal false, topic.written_on.respond_to?(:time_zone)
+ assert_not_respond_to topic.written_on, :time_zone
end
ensure
Topic.skip_time_zone_conversion_for_attributes = []
@@ -261,7 +261,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.zone.local(2000, 1, 1, 16, 24, 0), topic.bonus_time
- assert_not topic.bonus_time.utc?
+ assert_not_predicate topic.bonus_time, :utc?
attributes = {
"written_on(1i)" => "2000", "written_on(2i)" => "", "written_on(3i)" => "",
@@ -394,6 +394,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
"written_on(4i)" => "13",
"written_on(5i)" => "55",
)
- refute_predicate topic, :written_on_came_from_user?
+ assert_not_predicate topic, :written_on_came_from_user?
end
end
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index a2ccb603a9..32af90caef 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -36,7 +36,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
pirate.birds_with_reject_all_blank_attributes = [{ name: "", color: "", _destroy: "0" }]
pirate.save!
- assert pirate.birds_with_reject_all_blank.empty?
+ assert_empty pirate.birds_with_reject_all_blank
end
def test_should_not_build_a_new_record_if_reject_all_blank_returns_false
@@ -44,7 +44,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
pirate.birds_with_reject_all_blank_attributes = [{ name: "", color: "" }]
pirate.save!
- assert pirate.birds_with_reject_all_blank.empty?
+ assert_empty pirate.birds_with_reject_all_blank
end
def test_should_build_a_new_record_if_reject_all_blank_does_not_return_false
@@ -83,7 +83,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
def test_a_model_should_respond_to_underscore_destroy_and_return_if_it_is_marked_for_destruction
ship = Ship.create!(name: "Nights Dirty Lightning")
- assert !ship._destroy
+ assert_not ship._destroy
ship.mark_for_destruction
assert ship._destroy
end
@@ -152,7 +152,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
man = Man.create(name: "Jon")
interest = man.interests.create(topic: "the ladies")
man.update(interests_attributes: { _destroy: "1", id: interest.id })
- assert man.reload.interests.empty?
+ assert_empty man.reload.interests
end
def test_reject_if_is_not_short_circuited_if_allow_destroy_is_false
@@ -240,7 +240,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
@ship.destroy
@pirate.reload.ship_attributes = { name: "Davy Jones Gold Dagger" }
- assert !@pirate.ship.persisted?
+ assert_not_predicate @pirate.ship, :persisted?
assert_equal "Davy Jones Gold Dagger", @pirate.ship.name
end
@@ -261,7 +261,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_replace_an_existing_record_if_there_is_no_id
@pirate.reload.ship_attributes = { name: "Davy Jones Gold Dagger" }
- assert !@pirate.ship.persisted?
+ assert_not_predicate @pirate.ship, :persisted?
assert_equal "Davy Jones Gold Dagger", @pirate.ship.name
assert_equal "Nights Dirty Lightning", @ship.name
end
@@ -335,7 +335,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_also_work_with_a_HashWithIndifferentAccess
@pirate.ship_attributes = ActiveSupport::HashWithIndifferentAccess.new(id: @ship.id, name: "Davy Jones Gold Dagger")
- assert @pirate.ship.persisted?
+ assert_predicate @pirate.ship, :persisted?
assert_equal "Davy Jones Gold Dagger", @pirate.ship.name
end
@@ -350,12 +350,12 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
@pirate.attributes = { ship_attributes: { id: @ship.id, _destroy: "1" } }
- assert !@pirate.ship.destroyed?
- assert @pirate.ship.marked_for_destruction?
+ assert_not_predicate @pirate.ship, :destroyed?
+ assert_predicate @pirate.ship, :marked_for_destruction?
@pirate.save
- assert @pirate.ship.destroyed?
+ assert_predicate @pirate.ship, :destroyed?
assert_nil @pirate.reload.ship
end
@@ -424,7 +424,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
@pirate.destroy
@ship.reload.pirate_attributes = { catchphrase: "Arr" }
- assert !@ship.pirate.persisted?
+ assert_not_predicate @ship.pirate, :persisted?
assert_equal "Arr", @ship.pirate.catchphrase
end
@@ -445,7 +445,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_replace_an_existing_record_if_there_is_no_id
@ship.reload.pirate_attributes = { catchphrase: "Arr" }
- assert !@ship.pirate.persisted?
+ assert_not_predicate @ship.pirate, :persisted?
assert_equal "Arr", @ship.pirate.catchphrase
assert_equal "Aye", @pirate.catchphrase
end
@@ -550,7 +550,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
@pirate.delete
@ship.reload.attributes = { update_only_pirate_attributes: { catchphrase: "Arr" } }
- assert !@ship.update_only_pirate.persisted?
+ assert_not_predicate @ship.update_only_pirate, :persisted?
end
def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
@@ -632,10 +632,10 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_not_load_association_when_updating_existing_records
@pirate.reload
@pirate.send(association_setter, [{ id: @child_1.id, name: "Grace OMalley" }])
- assert ! @pirate.send(@association_name).loaded?
+ assert_not_predicate @pirate.send(@association_name), :loaded?
@pirate.save
- assert ! @pirate.send(@association_name).loaded?
+ assert_not_predicate @pirate.send(@association_name), :loaded?
assert_equal "Grace OMalley", @child_1.reload.name
end
@@ -663,7 +663,7 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_not_remove_scheduled_destroys_when_loading_association
@pirate.reload
@pirate.send(association_setter, [{ id: @child_1.id, _destroy: "1" }])
- assert @pirate.send(@association_name).load_target.find { |r| r.id == @child_1.id }.marked_for_destruction?
+ assert_predicate @pirate.send(@association_name).load_target.find { |r| r.id == @child_1.id }, :marked_for_destruction?
end
def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models
@@ -705,10 +705,10 @@ module NestedAttributesOnACollectionAssociationTests
association_getter => { "foo" => { name: "Grace OMalley" }, "bar" => { name: "Privateers Greed" } }
}
- assert !@pirate.send(@association_name).first.persisted?
+ assert_not_predicate @pirate.send(@association_name).first, :persisted?
assert_equal "Grace OMalley", @pirate.send(@association_name).first.name
- assert !@pirate.send(@association_name).last.persisted?
+ assert_not_predicate @pirate.send(@association_name).last, :persisted?
assert_equal "Privateers Greed", @pirate.send(@association_name).last.name
end
@@ -835,7 +835,7 @@ module NestedAttributesOnACollectionAssociationTests
man = Man.create(name: "John")
interest = man.interests.create(topic: "bar", zine_id: 0)
assert interest.save
- assert !man.update(interests_attributes: { id: interest.id, zine_id: "foo" })
+ assert_not man.update(interests_attributes: { id: interest.id, zine_id: "foo" })
end
end
@@ -1091,7 +1091,7 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR
test "nested singular associations are validated" do
part = ShipPart.new(name: "Stern", ship_attributes: { name: nil })
- assert_not part.valid?
+ assert_not_predicate part, :valid?
assert_equal ["Ship name can't be blank"], part.errors.full_messages
end
end
diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
index f04c68b08f..1d26057fdc 100644
--- a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
+++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
@@ -63,7 +63,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
# Characterizing when :before_add callback is called
test ":before_add called for new bird when not loaded" do
- assert_not @pirate.birds_with_add.loaded?
+ assert_not_predicate @pirate.birds_with_add, :loaded?
@pirate.birds_with_add_attributes = new_bird_attributes
assert_new_bird_with_callback_called
end
@@ -80,7 +80,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
end
test ":before_add not called for identical assignment when not loaded" do
- assert_not @pirate.birds_with_add.loaded?
+ assert_not_predicate @pirate.birds_with_add, :loaded?
@pirate.birds_with_add_attributes = existing_birds_attributes
assert_callbacks_not_called
end
@@ -92,7 +92,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
end
test ":before_add not called for destroy assignment when not loaded" do
- assert_not @pirate.birds_with_add.loaded?
+ assert_not_predicate @pirate.birds_with_add, :loaded?
@pirate.birds_with_add_attributes = destroy_bird_attributes
assert_callbacks_not_called
end
@@ -111,7 +111,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
# Ensuring that the records in the association target are updated,
# whether the association is loaded before or not
test "Assignment updates records in target when not loaded" do
- assert_not @pirate.birds_with_add.loaded?
+ assert_not_predicate @pirate.birds_with_add, :loaded?
@pirate.birds_with_add_attributes = update_new_and_destroy_bird_attributes
assert_assignment_affects_records_in_target(:birds_with_add)
end
@@ -124,7 +124,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
test("Assignment updates records in target when not loaded" \
" and callback loads target") do
- assert_not @pirate.birds_with_add_load.loaded?
+ assert_not_predicate @pirate.birds_with_add_load, :loaded?
@pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes
assert_assignment_affects_records_in_target(:birds_with_add_load)
end
diff --git a/activerecord/test/cases/numeric_data_test.rb b/activerecord/test/cases/numeric_data_test.rb
index f917c8f727..14db63890e 100644
--- a/activerecord/test/cases/numeric_data_test.rb
+++ b/activerecord/test/cases/numeric_data_test.rb
@@ -19,7 +19,7 @@ class NumericDataTest < ActiveRecord::TestCase
m = NumericData.new(
bank_balance: 1586.43,
big_bank_balance: BigDecimal("1000234000567.95"),
- world_population: 6000000000,
+ world_population: 2**62,
my_house_population: 3
)
assert m.save
@@ -27,11 +27,8 @@ class NumericDataTest < ActiveRecord::TestCase
m1 = NumericData.find(m.id)
assert_not_nil m1
- # As with migration_test.rb, we should make world_population >= 2**62
- # to cover 64-bit platforms and test it is a Bignum, but the main thing
- # is that it's an Integer.
assert_kind_of Integer, m1.world_population
- assert_equal 6000000000, m1.world_population
+ assert_equal 2**62, m1.world_population
assert_kind_of Integer, m1.my_house_population
assert_equal 3, m1.my_house_population
@@ -47,7 +44,7 @@ class NumericDataTest < ActiveRecord::TestCase
m = NumericData.new(
bank_balance: 1586.43122334,
big_bank_balance: BigDecimal("234000567.952344"),
- world_population: 6000000000,
+ world_population: 2**62,
my_house_population: 3
)
assert m.save
@@ -55,11 +52,8 @@ class NumericDataTest < ActiveRecord::TestCase
m1 = NumericData.find(m.id)
assert_not_nil m1
- # As with migration_test.rb, we should make world_population >= 2**62
- # to cover 64-bit platforms and test it is a Bignum, but the main thing
- # is that it's an Integer.
assert_kind_of Integer, m1.world_population
- assert_equal 6000000000, m1.world_population
+ assert_equal 2**62, m1.world_population
assert_kind_of Integer, m1.my_house_population
assert_equal 3, m1.my_house_population
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index 6cbe18cc8c..e0c5725944 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -48,7 +48,7 @@ class PersistenceTest < ActiveRecord::TestCase
end
if test_update_with_order_succeeds.call("id DESC")
- assert !test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception
+ assert_not test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception
else
# test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead
assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do
@@ -59,18 +59,47 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_all_with_order_and_limit_updates_subset_only
author = authors(:david)
- assert_nothing_raised do
- assert_equal 1, author.posts_sorted_by_id_limited.size
- assert_equal 2, author.posts_sorted_by_id_limited.limit(2).to_a.size
- assert_equal 1, author.posts_sorted_by_id_limited.update_all([ "body = ?", "bulk update!" ])
- assert_equal "bulk update!", posts(:welcome).body
- assert_not_equal "bulk update!", posts(:thinking).body
- end
+ limited_posts = author.posts_sorted_by_id_limited
+ assert_equal 1, limited_posts.size
+ assert_equal 2, limited_posts.limit(2).size
+ assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ])
+ assert_equal "bulk update!", posts(:welcome).body
+ assert_not_equal "bulk update!", posts(:thinking).body
+ end
+
+ def test_update_all_with_order_and_limit_and_offset_updates_subset_only
+ author = authors(:david)
+ limited_posts = author.posts_sorted_by_id_limited.offset(1)
+ assert_equal 1, limited_posts.size
+ assert_equal 2, limited_posts.limit(2).size
+ assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ])
+ assert_equal "bulk update!", posts(:thinking).body
+ assert_not_equal "bulk update!", posts(:welcome).body
+ end
+
+ def test_delete_all_with_order_and_limit_deletes_subset_only
+ author = authors(:david)
+ limited_posts = Post.where(author: author).order(:id).limit(1)
+ assert_equal 1, limited_posts.size
+ assert_equal 2, limited_posts.limit(2).size
+ assert_equal 1, limited_posts.delete_all
+ assert_raise(ActiveRecord::RecordNotFound) { posts(:welcome) }
+ assert posts(:thinking)
+ end
+
+ def test_delete_all_with_order_and_limit_and_offset_deletes_subset_only
+ author = authors(:david)
+ limited_posts = Post.where(author: author).order(:id).limit(1).offset(1)
+ assert_equal 1, limited_posts.size
+ assert_equal 2, limited_posts.limit(2).size
+ assert_equal 1, limited_posts.delete_all
+ assert_raise(ActiveRecord::RecordNotFound) { posts(:thinking) }
+ assert posts(:welcome)
end
end
def test_update_many
- topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" }, nil => {} }
+ topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } }
updated = Topic.update(topic_data.keys, topic_data.values)
assert_equal [1, 2], updated.map(&:id)
@@ -78,10 +107,33 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal "2 updated", Topic.find(2).content
end
+ def test_update_many_with_duplicated_ids
+ updated = Topic.update([1, 1, 2], [
+ { "content" => "1 duplicated" }, { "content" => "1 updated" }, { "content" => "2 updated" }
+ ])
+
+ assert_equal [1, 1, 2], updated.map(&:id)
+ assert_equal "1 updated", Topic.find(1).content
+ assert_equal "2 updated", Topic.find(2).content
+ end
+
+ def test_update_many_with_invalid_id
+ topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" }, 99999 => {} }
+
+ assert_raise(ActiveRecord::RecordNotFound) do
+ Topic.update(topic_data.keys, topic_data.values)
+ end
+
+ assert_not_equal "1 updated", Topic.find(1).content
+ assert_not_equal "2 updated", Topic.find(2).content
+ end
+
def test_class_level_update_is_affected_by_scoping
topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } }
- assert_equal [], Topic.where("1=0").scoping { Topic.update(topic_data.keys, topic_data.values) }
+ assert_raise(ActiveRecord::RecordNotFound) do
+ Topic.where("1=0").scoping { Topic.update(topic_data.keys, topic_data.values) }
+ end
assert_not_equal "1 updated", Topic.find(1).content
assert_not_equal "2 updated", Topic.find(2).content
@@ -94,27 +146,31 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_delete_all_with_joins_and_where_part_is_hash
- where_args = { toys: { name: "Bone" } }
- count = Pet.joins(:toys).where(where_args).count
+ pets = Pet.joins(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
+ end
+
+ def test_delete_all_with_joins_and_where_part_is_not_hash
+ pets = Pet.joins(:toys).where("toys.name = ?", "Bone")
- assert_equal count, 1
- assert_equal count, Pet.joins(:toys).where(where_args).delete_all
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
end
def test_delete_all_with_left_joins
- where_args = { toys: { name: "Bone" } }
- count = Pet.left_joins(:toys).where(where_args).count
+ pets = Pet.left_joins(:toys).where(toys: { name: "Bone" })
- assert_equal count, 1
- assert_equal count, Pet.left_joins(:toys).where(where_args).delete_all
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
end
- def test_delete_all_with_joins_and_where_part_is_not_hash
- where_args = ["toys.name = ?", "Bone"]
- count = Pet.joins(:toys).where(where_args).count
+ def test_delete_all_with_includes
+ pets = Pet.includes(:toys).where(toys: { name: "Bone" })
- assert_equal count, 1
- assert_equal count, Pet.joins(:toys).where(where_args).delete_all
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
end
def test_increment_attribute
@@ -161,7 +217,7 @@ class PersistenceTest < ActiveRecord::TestCase
def test_destroy_all
conditions = "author_name = 'Mary'"
topics_by_mary = Topic.all.merge!(where: conditions, order: "id").to_a
- assert ! topics_by_mary.empty?
+ assert_not_empty topics_by_mary
assert_difference("Topic.count", -topics_by_mary.size) do
destroyed = Topic.where(conditions).destroy_all.sort_by(&:id)
@@ -171,23 +227,40 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_destroy_many
- clients = Client.all.merge!(order: "id").find([2, 3])
+ clients = Client.find([2, 3])
assert_difference("Client.count", -2) do
- destroyed = Client.destroy([2, 3, nil]).sort_by(&:id)
+ destroyed = Client.destroy([2, 3])
assert_equal clients, destroyed
assert destroyed.all?(&:frozen?), "destroyed clients should be frozen"
end
end
+ def test_destroy_many_with_invalid_id
+ clients = Client.find([2, 3])
+
+ assert_raise(ActiveRecord::RecordNotFound) do
+ Client.destroy([2, 3, 99999])
+ end
+
+ assert_equal clients, Client.find([2, 3])
+ end
+
def test_becomes
assert_kind_of Reply, topics(:first).becomes(Reply)
assert_equal "The First Topic", topics(:first).becomes(Reply).title
end
+ def test_becomes_after_reload_schema_from_cache
+ Reply.define_attribute_methods
+ Reply.serialize(:content) # invoke reload_schema_from_cache
+ assert_kind_of Reply, topics(:first).becomes(Reply)
+ assert_equal "The First Topic", topics(:first).becomes(Reply).title
+ end
+
def test_becomes_includes_errors
company = Company.new(name: nil)
- assert !company.valid?
+ assert_not_predicate company, :valid?
original_errors = company.errors
client = company.becomes(Client)
assert_equal original_errors.keys, client.errors.keys
@@ -304,8 +377,8 @@ class PersistenceTest < ActiveRecord::TestCase
developer.destroy
new_developer = developer.dup
new_developer.save
- assert new_developer.persisted?
- assert_not new_developer.destroyed?
+ assert_predicate new_developer, :persisted?
+ assert_not_predicate new_developer, :destroyed?
end
def test_create_many
@@ -316,12 +389,13 @@ class PersistenceTest < ActiveRecord::TestCase
def test_create_columns_not_equal_attributes
topic = Topic.instantiate(
- "attributes" => {
- "title" => "Another New Topic",
- "does_not_exist" => "test"
- }
+ "title" => "Another New Topic",
+ "does_not_exist" => "test"
)
+ topic = topic.dup # reset @new_record
assert_nothing_raised { topic.save }
+ assert_predicate topic, :persisted?
+ assert_equal "Another New Topic", topic.reload.title
end
def test_create_through_factory_with_block
@@ -368,6 +442,8 @@ class PersistenceTest < ActiveRecord::TestCase
topic_reloaded = Topic.instantiate(topic.attributes.merge("does_not_exist" => "test"))
topic_reloaded.title = "A New Topic"
assert_nothing_raised { topic_reloaded.save }
+ assert_predicate topic_reloaded, :persisted?
+ assert_equal "A New Topic", topic_reloaded.reload.title
end
def test_update_for_record_with_only_primary_key
@@ -404,6 +480,22 @@ class PersistenceTest < ActiveRecord::TestCase
assert_instance_of Reply, Reply.find(reply.id)
end
+ def test_becomes_default_sti_subclass
+ original_type = Topic.columns_hash["type"].default
+ ActiveRecord::Base.connection.change_column_default :topics, :type, "Reply"
+ Topic.reset_column_information
+
+ reply = topics(:second)
+ assert_instance_of Reply, reply
+
+ topic = reply.becomes(Topic)
+ assert_instance_of Topic, topic
+
+ ensure
+ ActiveRecord::Base.connection.change_column_default :topics, :type, original_type
+ Topic.reset_column_information
+ end
+
def test_update_after_create
klass = Class.new(Topic) do
def self.name; "Topic"; end
@@ -433,7 +525,7 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_does_not_run_sql_if_record_has_not_changed
topic = Topic.create(title: "Another New Topic")
assert_queries(0) { assert topic.update(title: "Another New Topic") }
- assert_queries(0) { assert topic.update_attributes(title: "Another New Topic") }
+ assert_queries(0) { assert topic.update(title: "Another New Topic") }
end
def test_delete
@@ -469,10 +561,18 @@ class PersistenceTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) }
end
- def test_record_not_found_exception
+ def test_find_raises_record_not_found_exception
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(99999) }
end
+ def test_update_raises_record_not_found_exception
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.update(99999, approved: true) }
+ end
+
+ def test_destroy_raises_record_not_found_exception
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.destroy(99999) }
+ end
+
def test_update_all
assert_equal Topic.count, Topic.update_all("content = 'bulk updated!'")
assert_equal "bulk updated!", Topic.find(1).content
@@ -496,17 +596,24 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_update_all_with_joins
- where_args = { toys: { name: "Bone" } }
- count = Pet.left_joins(:toys).where(where_args).count
+ pets = Pet.joins(:toys).where(toys: { name: "Bone" })
- assert_equal count, Pet.joins(:toys).where(where_args).update_all(name: "Bob")
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.update_all(name: "Bob")
end
def test_update_all_with_left_joins
- where_args = { toys: { name: "Bone" } }
- count = Pet.left_joins(:toys).where(where_args).count
+ pets = Pet.left_joins(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.update_all(name: "Bob")
+ end
+
+ def test_update_all_with_includes
+ pets = Pet.includes(:toys).where(toys: { name: "Bone" })
- assert_equal count, Pet.left_joins(:toys).where(where_args).update_all(name: "Bob")
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.update_all(name: "Bob")
end
def test_update_all_with_non_standard_table_name
@@ -515,40 +622,65 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_delete_new_record
- client = Client.new
+ client = Client.new(name: "37signals")
client.delete
- assert client.frozen?
+ assert_predicate client, :frozen?
+
+ assert_not client.save
+ assert_raise(ActiveRecord::RecordNotSaved) { client.save! }
+
+ assert_predicate client, :frozen?
+ assert_raise(RuntimeError) { client.name = "something else" }
end
def test_delete_record_with_associations
client = Client.find(3)
client.delete
- assert client.frozen?
+ assert_predicate client, :frozen?
assert_kind_of Firm, client.firm
+
+ assert_not client.save
+ assert_raise(ActiveRecord::RecordNotSaved) { client.save! }
+
+ assert_predicate client, :frozen?
assert_raise(RuntimeError) { client.name = "something else" }
end
def test_destroy_new_record
- client = Client.new
+ client = Client.new(name: "37signals")
client.destroy
- assert client.frozen?
+ assert_predicate client, :frozen?
+
+ assert_not client.save
+ assert_raise(ActiveRecord::RecordNotSaved) { client.save! }
+
+ assert_predicate client, :frozen?
+ assert_raise(RuntimeError) { client.name = "something else" }
end
def test_destroy_record_with_associations
client = Client.find(3)
client.destroy
- assert client.frozen?
+ assert_predicate client, :frozen?
assert_kind_of Firm, client.firm
+
+ assert_not client.save
+ assert_raise(ActiveRecord::RecordNotSaved) { client.save! }
+
+ assert_predicate client, :frozen?
assert_raise(RuntimeError) { client.name = "something else" }
end
def test_update_attribute
- assert !Topic.find(1).approved?
+ assert_not_predicate Topic.find(1), :approved?
Topic.find(1).update_attribute("approved", true)
- assert Topic.find(1).approved?
+ assert_predicate Topic.find(1), :approved?
Topic.find(1).update_attribute(:approved, false)
- assert !Topic.find(1).approved?
+ assert_not_predicate Topic.find(1), :approved?
+
+ Topic.find(1).update_attribute(:change_approved_before_save, true)
+ assert_predicate Topic.find(1), :approved?
end
def test_update_attribute_for_readonly_attribute
@@ -585,14 +717,14 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_column
topic = Topic.find(1)
topic.update_column("approved", true)
- assert topic.approved?
+ assert_predicate topic, :approved?
topic.reload
- assert topic.approved?
+ assert_predicate topic, :approved?
topic.update_column(:approved, false)
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
topic.reload
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
end
def test_update_column_should_not_use_setter_method
@@ -679,10 +811,10 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_columns
topic = Topic.find(1)
topic.update_columns("approved" => true, title: "Sebastian Topic")
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_equal "Sebastian Topic", topic.title
topic.reload
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_equal "Sebastian Topic", topic.title
end
@@ -790,54 +922,45 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update
topic = Topic.find(1)
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
assert_equal "The First Topic", topic.title
topic.update("approved" => true, "title" => "The First Topic Updated")
topic.reload
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_equal "The First Topic Updated", topic.title
topic.update(approved: false, title: "The First Topic")
topic.reload
- assert !topic.approved?
- assert_equal "The First Topic", topic.title
- end
-
- def test_update_attributes
- topic = Topic.find(1)
- assert !topic.approved?
- assert_equal "The First Topic", topic.title
-
- topic.update_attributes("approved" => true, "title" => "The First Topic Updated")
- topic.reload
- assert topic.approved?
- assert_equal "The First Topic Updated", topic.title
-
- topic.update_attributes(approved: false, title: "The First Topic")
- topic.reload
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
assert_equal "The First Topic", topic.title
error = assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do
- topic.update_attributes(id: 3, title: "Hm is it possible?")
+ topic.update(id: 3, title: "Hm is it possible?")
end
assert_not_nil error.cause
assert_not_equal "Hm is it possible?", Topic.find(3).title
- topic.update_attributes(id: 1234)
+ topic.update(id: 1234)
assert_nothing_raised { topic.reload }
assert_equal topic.title, Topic.find(1234).title
end
- def test_update_attributes_parameters
+ def test_update_attributes
+ topic = Topic.find(1)
+ assert_deprecated do
+ topic.update_attributes("title" => "The First Topic Updated")
+ end
+ end
+
+ def test_update_parameters
topic = Topic.find(1)
assert_nothing_raised do
- topic.update_attributes({})
+ topic.update({})
end
assert_raises(ArgumentError) do
- topic.update_attributes(nil)
+ topic.update(nil)
end
end
@@ -863,24 +986,10 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_update_attributes!
- Reply.validates_presence_of(:title)
reply = Reply.find(2)
- assert_equal "The Second Topic of the day", reply.title
- assert_equal "Have a nice day", reply.content
-
- reply.update_attributes!("title" => "The Second Topic of the day updated", "content" => "Have a nice evening")
- reply.reload
- assert_equal "The Second Topic of the day updated", reply.title
- assert_equal "Have a nice evening", reply.content
-
- reply.update_attributes!(title: "The Second Topic of the day", content: "Have a nice day")
- reply.reload
- assert_equal "The Second Topic of the day", reply.title
- assert_equal "Have a nice day", reply.content
-
- assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(title: nil, content: "Have a nice evening") }
- ensure
- Reply.clear_validators!
+ assert_deprecated do
+ reply.update_attributes!("title" => "The Second Topic of the day updated")
+ end
end
def test_destroyed_returns_boolean
@@ -917,7 +1026,7 @@ class PersistenceTest < ActiveRecord::TestCase
Topic.find(1).replies << should_be_destroyed_reply
topic = Topic.destroy(1)
- assert topic.destroyed?
+ assert_predicate topic, :destroyed?
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) }
assert_raise(ActiveRecord::RecordNotFound) { Reply.find(should_be_destroyed_reply.id) }
@@ -927,7 +1036,9 @@ class PersistenceTest < ActiveRecord::TestCase
should_not_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world")
Topic.find(1).replies << should_not_be_destroyed_reply
- assert_nil Topic.where("1=0").scoping { Topic.destroy(1) }
+ assert_raise(ActiveRecord::RecordNotFound) do
+ Topic.where("1=0").scoping { Topic.destroy(1) }
+ end
assert_nothing_raised { Topic.find(1) }
assert_nothing_raised { Reply.find(should_not_be_destroyed_reply.id) }
@@ -995,13 +1106,13 @@ class PersistenceTest < ActiveRecord::TestCase
def test_find_via_reload
post = Post.new
- assert post.new_record?
+ assert_predicate post, :new_record?
post.id = 1
post.reload
assert_equal "Welcome to the weblog", post.title
- assert_not post.new_record?
+ assert_not_predicate post, :new_record?
end
def test_reload_via_querycache
@@ -1049,13 +1160,18 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_reset_column_information_resets_children
- child = Class.new(Topic)
- child.new # force schema to load
+ child_class = Class.new(Topic)
+ child_class.new # force schema to load
ActiveRecord::Base.connection.add_column(:topics, :foo, :string)
Topic.reset_column_information
- assert_equal "bar", child.new(foo: :bar).foo
+ # this should redefine attribute methods
+ child_class.new
+
+ assert child_class.instance_methods.include?(:foo)
+ assert child_class.instance_methods.include?(:foo_changed?)
+ assert_equal "bar", child_class.new(foo: :bar).foo
ensure
ActiveRecord::Base.connection.remove_column(:topics, :foo)
Topic.reset_column_information
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index a36d786b40..60dac91ec9 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -156,10 +156,6 @@ class PrimaryKeysTest < ActiveRecord::TestCase
assert_nothing_raised { MixedCaseMonkey.find(1).destroy }
end
- def test_deprecate_supports_primary_key
- assert_deprecated { ActiveRecord::Base.connection.supports_primary_key? }
- end
-
def test_primary_key_returns_value_if_it_exists
klass = Class.new(ActiveRecord::Base) do
self.table_name = "developers"
@@ -211,13 +207,13 @@ class PrimaryKeysTest < ActiveRecord::TestCase
def test_serial_with_quoted_sequence_name
column = MixedCaseMonkey.columns_hash[MixedCaseMonkey.primary_key]
assert_equal "nextval('\"mixed_case_monkeys_monkeyID_seq\"'::regclass)", column.default_function
- assert column.serial?
+ assert_predicate column, :serial?
end
def test_serial_with_unquoted_sequence_name
column = Topic.columns_hash[Topic.primary_key]
assert_equal "nextval('topics_id_seq'::regclass)", column.default_function
- assert column.serial?
+ assert_predicate column, :serial?
end
end
end
@@ -302,6 +298,8 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
assert_not column.null
assert_equal :string, column.type
assert_equal 42, column.limit
+ ensure
+ Barcode.reset_column_information
end
test "schema dump primary key includes type and options" do
@@ -432,7 +430,7 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter)
@connection.create_table(:widgets, id: @pk_type, force: true)
column = @connection.columns(:widgets).find { |c| c.name == "id" }
assert_equal :integer, column.type
- assert_not column.bigint?
+ assert_not_predicate column, :bigint?
end
test "primary key with serial/integer are automatically numbered" do
@@ -451,10 +449,10 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter)
test "primary key column type with options" do
@connection.create_table(:widgets, id: :primary_key, limit: 4, unsigned: true, force: true)
column = @connection.columns(:widgets).find { |c| c.name == "id" }
- assert column.auto_increment?
+ assert_predicate column, :auto_increment?
assert_equal :integer, column.type
- assert_not column.bigint?
- assert column.unsigned?
+ assert_not_predicate column, :bigint?
+ assert_predicate column, :unsigned?
schema = dump_table_schema "widgets"
assert_match %r{create_table "widgets", id: :integer, unsigned: true, }, schema
@@ -463,10 +461,10 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter)
test "bigint primary key with unsigned" do
@connection.create_table(:widgets, id: :bigint, unsigned: true, force: true)
column = @connection.columns(:widgets).find { |c| c.name == "id" }
- assert column.auto_increment?
+ assert_predicate column, :auto_increment?
assert_equal :integer, column.type
- assert column.bigint?
- assert column.unsigned?
+ assert_predicate column, :bigint?
+ assert_predicate column, :unsigned?
schema = dump_table_schema "widgets"
assert_match %r{create_table "widgets", id: :bigint, unsigned: true, }, schema
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 5cb537b623..393f363e37 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -82,7 +82,7 @@ class QueryCacheTest < ActiveRecord::TestCase
assert_cache :off, conn
end
- assert !ActiveRecord::Base.connection.nil?
+ assert_not_predicate ActiveRecord::Base.connection, :nil?
assert_cache :off
middleware {
@@ -283,7 +283,7 @@ class QueryCacheTest < ActiveRecord::TestCase
payload[:sql].downcase!
end
- assert_raises RuntimeError do
+ assert_raises frozen_error_class do
ActiveRecord::Base.cache do
assert_queries(1) { Task.find(1); Task.find(1) }
end
@@ -320,6 +320,17 @@ class QueryCacheTest < ActiveRecord::TestCase
end
end
+ def test_cache_is_available_when_connection_is_connected
+ conf = ActiveRecord::Base.configurations
+
+ ActiveRecord::Base.configurations = {}
+ Task.cache do
+ assert_queries(1) { Task.find(1); Task.find(1) }
+ end
+ ensure
+ ActiveRecord::Base.configurations = conf
+ end
+
def test_cache_is_available_when_using_a_not_connected_connection
skip "In-Memory DB can't test for using a not connected connection" if in_memory_db?
with_temporary_connection_pool do
@@ -327,7 +338,7 @@ class QueryCacheTest < ActiveRecord::TestCase
conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2")
ActiveRecord::Base.connection_handler.establish_connection(conf)
Task.connection_specification_name = "test2"
- refute Task.connected?
+ assert_not_predicate Task, :connected?
Task.cache do
begin
@@ -366,7 +377,7 @@ class QueryCacheTest < ActiveRecord::TestCase
post = Post.first
Post.transaction do
- post.update_attributes(title: "rollback")
+ post.update(title: "rollback")
assert_equal 1, Post.where(title: "rollback").to_a.count
raise ActiveRecord::Rollback
end
@@ -379,7 +390,7 @@ class QueryCacheTest < ActiveRecord::TestCase
begin
Post.transaction do
- post.update_attributes(title: "rollback")
+ post.update(title: "rollback")
assert_equal 1, Post.where(title: "rollback").to_a.count
raise "broken"
end
@@ -414,24 +425,25 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_query_cache_does_not_establish_connection_if_unconnected
with_temporary_connection_pool do
ActiveRecord::Base.clear_active_connections!
- refute ActiveRecord::Base.connection_handler.active_connections? # sanity check
+ assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check
middleware {
- refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup"
+ assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup"
}.call({})
- refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup"
+ assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup"
end
end
def test_query_cache_is_enabled_on_connections_established_after_middleware_runs
with_temporary_connection_pool do
ActiveRecord::Base.clear_active_connections!
- refute ActiveRecord::Base.connection_handler.active_connections? # sanity check
+ assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check
middleware {
- assert ActiveRecord::Base.connection.query_cache_enabled, "QueryCache did not get lazily enabled"
+ assert_predicate ActiveRecord::Base.connection, :query_cache_enabled
}.call({})
+ assert_not_predicate ActiveRecord::Base.connection, :query_cache_enabled
end
end
@@ -444,14 +456,23 @@ class QueryCacheTest < ActiveRecord::TestCase
assert ActiveRecord::Base.connection.query_cache_enabled
Thread.new {
- refute ActiveRecord::Base.connection_pool.query_cache_enabled
- refute ActiveRecord::Base.connection.query_cache_enabled
+ assert_not ActiveRecord::Base.connection_pool.query_cache_enabled
+ assert_not ActiveRecord::Base.connection.query_cache_enabled
}.join
}.call({})
end
end
+ def test_query_cache_is_enabled_on_all_connection_pools
+ middleware {
+ ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
+ assert pool.query_cache_enabled
+ assert pool.connection.query_cache_enabled
+ end
+ }.call({})
+ end
+
private
def middleware(&app)
executor = Class.new(ActiveSupport::Executor)
@@ -497,19 +518,19 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_find
assert_called(Task.connection, :clear_query_cache) do
- assert !Task.connection.query_cache_enabled
+ assert_not Task.connection.query_cache_enabled
Task.cache do
assert Task.connection.query_cache_enabled
Task.find(1)
Task.uncached do
- assert !Task.connection.query_cache_enabled
+ assert_not Task.connection.query_cache_enabled
Task.find(1)
end
assert Task.connection.query_cache_enabled
end
- assert !Task.connection.query_cache_enabled
+ assert_not Task.connection.query_cache_enabled
end
end
@@ -553,7 +574,7 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do
ActiveRecord::Base.cache do
p = Post.find(1)
- assert p.categories.any?
+ assert_predicate p.categories, :any?
p.categories.delete_all
end
end
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index 59d3bbb573..92eb0c814f 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -46,27 +46,60 @@ module ActiveRecord
assert_equal t.to_s(:db), @quoter.quoted_date(t)
end
- def test_quoted_time_utc
+ def test_quoted_timestamp_utc
with_timezone_config default: :utc do
t = Time.now.change(usec: 0)
assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
end
end
- def test_quoted_time_local
+ def test_quoted_timestamp_local
with_timezone_config default: :local do
t = Time.now.change(usec: 0)
assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
end
end
- def test_quoted_time_crazy
+ def test_quoted_timestamp_crazy
with_timezone_config default: :asdfasdf do
t = Time.now.change(usec: 0)
assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
end
end
+ def test_quoted_time_utc
+ with_timezone_config default: :utc do
+ t = Time.now.change(usec: 0)
+
+ expected = t.getutc.change(year: 2000, month: 1, day: 1)
+ expected = expected.to_s(:db).sub("2000-01-01 ", "")
+
+ assert_equal expected, @quoter.quoted_time(t)
+ end
+ end
+
+ def test_quoted_time_local
+ with_timezone_config default: :local do
+ t = Time.now.change(usec: 0)
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getlocal.to_s(:db).sub("2000-01-01 ", "")
+
+ assert_equal expected, @quoter.quoted_time(t)
+ end
+ end
+
+ def test_quoted_time_crazy
+ with_timezone_config default: :asdfasdf do
+ t = Time.now.change(usec: 0)
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getlocal.to_s(:db).sub("2000-01-01 ", "")
+
+ assert_equal expected, @quoter.quoted_time(t)
+ end
+ end
+
def test_quoted_datetime_utc
with_timezone_config default: :utc do
t = Time.now.change(usec: 0).to_datetime
@@ -83,23 +116,6 @@ module ActiveRecord
end
end
- class QuotedOne
- def quoted_id
- 1
- end
- end
- class SubQuotedOne < QuotedOne
- end
- def test_quote_with_quoted_id
- assert_deprecated(/defined on \S+::QuotedOne at .*quoting_test\.rb:[0-9]/) do
- assert_equal 1, @quoter.quote(QuotedOne.new)
- end
-
- assert_deprecated(/defined on \S+::SubQuotedOne\(\S+::QuotedOne\) at .*quoting_test\.rb:[0-9]/) do
- assert_equal 1, @quoter.quote(SubQuotedOne.new)
- end
- end
-
def test_quote_nil
assert_equal "NULL", @quoter.quote(nil)
end
@@ -128,7 +144,7 @@ module ActiveRecord
end
def test_quote_bigdecimal
- bigdec = BigDecimal.new((1 << 100).to_s)
+ bigdec = BigDecimal((1 << 100).to_s)
assert_equal bigdec.to_s("F"), @quoter.quote(bigdec)
end
@@ -207,26 +223,6 @@ module ActiveRecord
obj = Class.new.new
assert_raise(TypeError) { @conn.type_cast(obj) }
end
-
- def test_type_cast_object_which_responds_to_quoted_id
- quoted_id_obj = Class.new {
- def quoted_id
- "'zomg'"
- end
-
- def id
- 10
- end
- }.new
- assert_equal 10, @conn.type_cast(quoted_id_obj)
-
- quoted_id_obj = Class.new {
- def quoted_id
- "'zomg'"
- end
- }.new
- assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) }
- end
end
class QuoteBooleanTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb
index d1b85cb4ef..059fa76132 100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
@@ -16,14 +16,14 @@ class ReadOnlyTest < ActiveRecord::TestCase
def test_cant_save_readonly_record
dev = Developer.find(1)
- assert !dev.readonly?
+ assert_not_predicate dev, :readonly?
dev.readonly!
- assert dev.readonly?
+ assert_predicate dev, :readonly?
assert_nothing_raised do
dev.name = "Luscious forbidden fruit."
- assert !dev.save
+ assert_not dev.save
dev.name = "Forbidden."
end
@@ -38,8 +38,8 @@ class ReadOnlyTest < ActiveRecord::TestCase
end
def test_find_with_readonly_option
- Developer.all.each { |d| assert !d.readonly? }
- Developer.readonly(false).each { |d| assert !d.readonly? }
+ Developer.all.each { |d| assert_not d.readonly? }
+ Developer.readonly(false).each { |d| assert_not d.readonly? }
Developer.readonly(true).each { |d| assert d.readonly? }
Developer.readonly.each { |d| assert d.readonly? }
end
@@ -54,56 +54,56 @@ class ReadOnlyTest < ActiveRecord::TestCase
def test_has_many_find_readonly
post = Post.find(1)
- assert !post.comments.empty?
- assert !post.comments.any?(&:readonly?)
- assert !post.comments.to_a.any?(&:readonly?)
+ assert_not_empty post.comments
+ assert_not post.comments.any?(&:readonly?)
+ assert_not post.comments.to_a.any?(&:readonly?)
assert post.comments.readonly(true).all?(&:readonly?)
end
def test_has_many_with_through_is_not_implicitly_marked_readonly
assert people = Post.find(1).people
- assert !people.any?(&:readonly?)
+ assert_not people.any?(&:readonly?)
end
def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_by_id
- assert !posts(:welcome).people.find(1).readonly?
+ assert_not_predicate posts(:welcome).people.find(1), :readonly?
end
def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_first
- assert !posts(:welcome).people.first.readonly?
+ assert_not_predicate posts(:welcome).people.first, :readonly?
end
def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_last
- assert !posts(:welcome).people.last.readonly?
+ assert_not_predicate posts(:welcome).people.last, :readonly?
end
def test_readonly_scoping
Post.where("1=1").scoping do
- assert !Post.find(1).readonly?
- assert Post.readonly(true).find(1).readonly?
- assert !Post.readonly(false).find(1).readonly?
+ assert_not_predicate Post.find(1), :readonly?
+ assert_predicate Post.readonly(true).find(1), :readonly?
+ assert_not_predicate Post.readonly(false).find(1), :readonly?
end
Post.joins(" ").scoping do
- assert !Post.find(1).readonly?
- assert Post.readonly.find(1).readonly?
- assert !Post.readonly(false).find(1).readonly?
+ assert_not_predicate Post.find(1), :readonly?
+ assert_predicate Post.readonly.find(1), :readonly?
+ assert_not_predicate Post.readonly(false).find(1), :readonly?
end
# Oracle barfs on this because the join includes unqualified and
# conflicting column names
unless current_adapter?(:OracleAdapter)
Post.joins(", developers").scoping do
- assert_not Post.find(1).readonly?
- assert Post.readonly.find(1).readonly?
- assert !Post.readonly(false).find(1).readonly?
+ assert_not_predicate Post.find(1), :readonly?
+ assert_predicate Post.readonly.find(1), :readonly?
+ assert_not_predicate Post.readonly(false).find(1), :readonly?
end
end
Post.readonly(true).scoping do
- assert Post.find(1).readonly?
- assert Post.readonly.find(1).readonly?
- assert !Post.readonly(false).find(1).readonly?
+ assert_predicate Post.find(1), :readonly?
+ assert_predicate Post.readonly.find(1), :readonly?
+ assert_not_predicate Post.readonly(false).find(1), :readonly?
end
end
@@ -111,10 +111,10 @@ class ReadOnlyTest < ActiveRecord::TestCase
developer = Developer.find(1)
project = Post.find(1)
- assert !developer.projects.all_as_method.first.readonly?
- assert !developer.projects.all_as_scope.first.readonly?
+ assert_not_predicate developer.projects.all_as_method.first, :readonly?
+ assert_not_predicate developer.projects.all_as_scope.first, :readonly?
- assert !project.comments.all_as_method.first.readonly?
- assert !project.comments.all_as_scope.first.readonly?
+ assert_not_predicate project.comments.all_as_method.first, :readonly?
+ assert_not_predicate project.comments.all_as_scope.first, :readonly?
end
end
diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb
index 49170abe6f..b034fe3e3b 100644
--- a/activerecord/test/cases/reaper_test.rb
+++ b/activerecord/test/cases/reaper_test.rb
@@ -18,6 +18,7 @@ module ActiveRecord
class FakePool
attr_reader :reaped
+ attr_reader :flushed
def initialize
@reaped = false
@@ -26,20 +27,24 @@ module ActiveRecord
def reap
@reaped = true
end
+
+ def flush
+ @flushed = true
+ end
end
# A reaper with nil time should never reap connections
def test_nil_time
fp = FakePool.new
- assert !fp.reaped
+ assert_not fp.reaped
reaper = ConnectionPool::Reaper.new(fp, nil)
reaper.run
- assert !fp.reaped
+ assert_not fp.reaped
end
def test_some_time
fp = FakePool.new
- assert !fp.reaped
+ assert_not fp.reaped
reaper = ConnectionPool::Reaper.new(fp, 0.0001)
reaper.run
@@ -47,6 +52,7 @@ module ActiveRecord
Thread.pass
end
assert fp.reaped
+ assert fp.flushed
end
def test_pool_has_reaper
@@ -73,14 +79,14 @@ module ActiveRecord
end
Thread.pass while conn.nil?
- assert conn.in_use?
+ assert_predicate conn, :in_use?
child.terminate
while conn.in_use?
Thread.pass
end
- assert !conn.in_use?
+ assert_not_predicate conn, :in_use?
end
end
end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 4cd2d3aedc..abadafbad4 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -25,7 +25,6 @@ require "models/chef"
require "models/department"
require "models/cake_designer"
require "models/drink_designer"
-require "models/mocktail_designer"
require "models/recipe"
class ReflectionTest < ActiveRecord::TestCase
@@ -67,13 +66,16 @@ class ReflectionTest < ActiveRecord::TestCase
def test_column_string_type_and_limit
assert_equal :string, @first.column_for_attribute("title").type
+ assert_equal :string, @first.column_for_attribute(:title).type
+ assert_equal :string, @first.type_for_attribute("title").type
+ assert_equal :string, @first.type_for_attribute(:title).type
assert_equal 250, @first.column_for_attribute("title").limit
end
def test_column_null_not_null
subscriber = Subscriber.first
assert subscriber.column_for_attribute("name").null
- assert !subscriber.column_for_attribute("nick").null
+ assert_not subscriber.column_for_attribute("nick").null
end
def test_human_name_for_column
@@ -82,6 +84,9 @@ class ReflectionTest < ActiveRecord::TestCase
def test_integer_columns
assert_equal :integer, @first.column_for_attribute("id").type
+ assert_equal :integer, @first.column_for_attribute(:id).type
+ assert_equal :integer, @first.type_for_attribute("id").type
+ assert_equal :integer, @first.type_for_attribute(:id).type
end
def test_non_existent_columns_return_null_object
@@ -90,6 +95,9 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal "attribute_that_doesnt_exist", column.name
assert_nil column.sql_type
assert_nil column.type
+
+ column = @first.column_for_attribute(:attribute_that_doesnt_exist)
+ assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column
end
def test_non_existent_types_are_identity_types
@@ -99,6 +107,11 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal object, type.deserialize(object)
assert_equal object, type.cast(object)
assert_equal object, type.serialize(object)
+
+ type = @first.type_for_attribute(:attribute_that_doesnt_exist)
+ assert_equal object, type.deserialize(object)
+ assert_equal object, type.cast(object)
+ assert_equal object, type.serialize(object)
end
def test_reflection_klass_for_nested_class_name
@@ -150,7 +163,7 @@ class ReflectionTest < ActiveRecord::TestCase
expected = Pirate.reflect_on_all_associations.select { |r| r.options[:autosave] }
received = Pirate.reflect_on_all_autosave_associations
- assert !received.empty?
+ assert_not_empty received
assert_not_equal Pirate.reflect_on_all_associations.length, received.length
assert_equal expected, received
end
@@ -254,50 +267,35 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal expected, actual
end
- def test_scope_chain
- expected = [
- [Tagging.reflect_on_association(:tag).scope, Post.reflect_on_association(:first_blue_tags).scope],
- [Post.reflect_on_association(:first_taggings).scope],
- [Author.reflect_on_association(:misc_posts).scope]
- ]
- actual = assert_deprecated do
- Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain
- end
- assert_equal expected, actual
-
- expected = [
- [
- Tagging.reflect_on_association(:blue_tag).scope,
- Post.reflect_on_association(:first_blue_tags_2).scope,
- Author.reflect_on_association(:misc_post_first_blue_tags_2).scope
- ],
- [],
- []
- ]
- actual = assert_deprecated do
- Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain
- end
- assert_equal expected, actual
- end
-
def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case
- @hotel = Hotel.create!
- @department = @hotel.departments.create!
- @department.chefs.create!(employable: CakeDesigner.create!)
- @department.chefs.create!(employable: DrinkDesigner.create!)
+ hotel = Hotel.create!
+ department = hotel.departments.create!
+ department.chefs.create!(employable: CakeDesigner.create!)
+ department.chefs.create!(employable: DrinkDesigner.create!)
- assert_equal 1, @hotel.cake_designers.size
- assert_equal 1, @hotel.drink_designers.size
- assert_equal 2, @hotel.chefs.size
+ assert_equal 1, hotel.cake_designers.size
+ assert_equal 1, hotel.cake_designers.count
+ assert_equal 1, hotel.drink_designers.size
+ assert_equal 1, hotel.drink_designers.count
+ assert_equal 2, hotel.chefs.size
+ assert_equal 2, hotel.chefs.count
end
def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case_and_sti
- @hotel = Hotel.create!
- @hotel.mocktail_designers << MocktailDesigner.create!
+ hotel = Hotel.create!
+ hotel.mocktail_designers << MocktailDesigner.create!
+
+ assert_equal 1, hotel.mocktail_designers.size
+ assert_equal 1, hotel.mocktail_designers.count
+ assert_equal 1, hotel.chef_lists.size
+ assert_equal 1, hotel.chef_lists.count
- assert_equal 1, @hotel.mocktail_designers.size
- assert_equal 1, @hotel.mocktail_designers.count
- assert_equal 1, @hotel.chef_lists.size
+ hotel.mocktail_designers = []
+
+ assert_equal 0, hotel.mocktail_designers.size
+ assert_equal 0, hotel.mocktail_designers.count
+ assert_equal 0, hotel.chef_lists.size
+ assert_equal 0, hotel.chef_lists.count
end
def test_scope_chain_of_polymorphic_association_does_not_leak_into_other_hmt_associations
@@ -317,12 +315,12 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_nested?
- assert !Author.reflect_on_association(:comments).nested?
- assert Author.reflect_on_association(:tags).nested?
+ assert_not_predicate Author.reflect_on_association(:comments), :nested?
+ assert_predicate Author.reflect_on_association(:tags), :nested?
# Only goes :through once, but the through_reflection is a has_and_belongs_to_many, so this is
# a nested through association
- assert Category.reflect_on_association(:post_comments).nested?
+ assert_predicate Category.reflect_on_association(:post_comments), :nested?
end
def test_association_primary_key
@@ -337,15 +335,6 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal "custom_primary_key", Author.reflect_on_association(:tags_with_primary_key).association_primary_key.to_s # nested
end
- def test_association_primary_key_type
- # Normal Association
- assert_equal :integer, Author.reflect_on_association(:posts).association_primary_key_type.type
- assert_equal :string, Author.reflect_on_association(:essay).association_primary_key_type.type
-
- # Through Association
- assert_equal :string, Author.reflect_on_association(:essay_category).association_primary_key_type.type
- end
-
def test_association_primary_key_raises_when_missing_primary_key
reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author)
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key }
@@ -366,42 +355,49 @@ class ReflectionTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key }
end
+ def test_type
+ assert_equal "taggable_type", Post.reflect_on_association(:taggings).type.to_s
+ assert_equal "imageable_class", Post.reflect_on_association(:images).type.to_s
+ assert_nil Post.reflect_on_association(:readers).type
+ end
+
def test_foreign_type
assert_equal "sponsorable_type", Sponsor.reflect_on_association(:sponsorable).foreign_type.to_s
assert_equal "sponsorable_type", Sponsor.reflect_on_association(:thing).foreign_type.to_s
+ assert_nil Sponsor.reflect_on_association(:sponsor_club).foreign_type
end
def test_collection_association
- assert Pirate.reflect_on_association(:birds).collection?
- assert Pirate.reflect_on_association(:parrots).collection?
+ assert_predicate Pirate.reflect_on_association(:birds), :collection?
+ assert_predicate Pirate.reflect_on_association(:parrots), :collection?
- assert !Pirate.reflect_on_association(:ship).collection?
- assert !Ship.reflect_on_association(:pirate).collection?
+ assert_not_predicate Pirate.reflect_on_association(:ship), :collection?
+ assert_not_predicate Ship.reflect_on_association(:pirate), :collection?
end
def test_default_association_validation
- assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm), :validate?
- assert !ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm).validate?
- assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm).validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm), :validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm), :validate?
end
def test_always_validate_association_if_explicit
- assert ActiveRecord::Reflection.create(:has_one, :client, nil, { validate: true }, Firm).validate?
- assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { validate: true }, Firm).validate?
- assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { validate: true }, Firm).validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { validate: true }, Firm), :validate?
+ assert_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { validate: true }, Firm), :validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { validate: true }, Firm), :validate?
end
def test_validate_association_if_autosave
- assert ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true }, Firm).validate?
- assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true }, Firm).validate?
- assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true }, Firm).validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true }, Firm), :validate?
+ assert_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true }, Firm), :validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true }, Firm), :validate?
end
def test_never_validate_association_if_explicit
- assert !ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true, validate: false }, Firm).validate?
- assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true, validate: false }, Firm).validate?
- assert !ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true, validate: false }, Firm).validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true, validate: false }, Firm), :validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true, validate: false }, Firm), :validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true, validate: false }, Firm), :validate?
end
def test_foreign_key
@@ -409,26 +405,15 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal "category_id", Post.reflect_on_association(:categorizations).foreign_key.to_s
end
- def test_through_reflection_scope_chain_does_not_modify_other_reflections
- orig_conds = assert_deprecated do
- Post.reflect_on_association(:first_blue_tags_2).scope_chain
- end.inspect
- assert_deprecated do
- Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain
- end
- assert_equal orig_conds, assert_deprecated {
- Post.reflect_on_association(:first_blue_tags_2).scope_chain
- }.inspect
- end
-
def test_symbol_for_class_name
assert_equal Client, Firm.reflect_on_association(:unsorted_clients_with_symbol).klass
end
def test_class_for_class_name
- assert_deprecated do
- assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { class_name: Client }, Firm), :validate?
+ error = assert_raises(ArgumentError) do
+ ActiveRecord::Reflection.create(:has_many, :clients, nil, { class_name: Client }, Firm)
end
+ assert_equal "A class was passed to `:class_name` but we are expecting a string.", error.message
end
def test_join_table
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
index 2696d1bb00..3f3d41980c 100644
--- a/activerecord/test/cases/relation/delegation_test.rb
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -54,4 +54,32 @@ module ActiveRecord
Comment.all
end
end
+
+ class QueryingMethodsDelegationTest < ActiveRecord::TestCase
+ QUERYING_METHODS = [
+ :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?,
+ :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!,
+ :first_or_create, :first_or_create!, :first_or_initialize,
+ :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by,
+ :find_by, :find_by!,
+ :destroy_all, :delete_all, :update_all,
+ :find_each, :find_in_batches, :in_batches,
+ :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or,
+ :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending,
+ :having, :create_with, :distinct, :references, :none, :unscope, :merge,
+ :count, :average, :minimum, :maximum, :sum, :calculate,
+ :pluck, :pick, :ids,
+ ]
+
+ def test_delegate_querying_methods
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "posts"
+ end
+
+ QUERYING_METHODS.each do |method|
+ assert_respond_to klass.all, method
+ assert_respond_to klass, method
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
index b68b3723f6..f53ef1fe35 100644
--- a/activerecord/test/cases/relation/merging_test.rb
+++ b/activerecord/test/cases/relation/merging_test.rb
@@ -58,7 +58,7 @@ class RelationMergingTest < ActiveRecord::TestCase
def test_relation_merging_with_locks
devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2))
- assert devs.locked?
+ assert_predicate devs, :locked?
end
def test_relation_merging_with_preload
@@ -72,6 +72,16 @@ class RelationMergingTest < ActiveRecord::TestCase
assert_equal 1, comments.count
end
+ def test_relation_merging_with_left_outer_joins
+ comments = Comment.joins(:post).where(body: "Thank you for the welcome").merge(Post.left_outer_joins(:author).where(body: "Such a lovely day"))
+
+ assert_equal 1, comments.count
+ end
+
+ def test_relation_merging_with_skip_query_cache
+ assert_equal Post.all.merge(Post.all.skip_query_cache!).skip_query_cache_value, true
+ end
+
def test_relation_merging_with_association
assert_queries(2) do # one for loading post, and another one merged query
post = Post.where(body: "Such a lovely day").first
@@ -107,9 +117,9 @@ class RelationMergingTest < ActiveRecord::TestCase
def test_merging_with_from_clause
relation = Post.all
- assert relation.from_clause.empty?
+ assert_empty relation.from_clause
relation = relation.merge(Post.from("posts"))
- refute relation.from_clause.empty?
+ assert_not_empty relation.from_clause
end
end
diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
index ad3700b73a..f82ecd4449 100644
--- a/activerecord/test/cases/relation/mutation_test.rb
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -25,7 +25,7 @@ module ActiveRecord
test "#order! with symbol prepends the table name" do
assert relation.order!(:name).equal?(relation)
node = relation.order_values.first
- assert node.ascending?
+ assert_predicate node, :ascending?
assert_equal :name, node.expr.name
assert_equal "posts", node.expr.relation.name
end
@@ -88,7 +88,7 @@ module ActiveRecord
assert relation.reorder!(:name).equal?(relation)
node = relation.order_values.first
- assert node.ascending?
+ assert_predicate node, :ascending?
assert_equal :name, node.expr.name
assert_equal "posts", node.expr.relation.name
end
@@ -137,9 +137,14 @@ module ActiveRecord
assert relation.skip_query_cache_value
end
+ test "skip_preloading!" do
+ relation.skip_preloading!
+ assert relation.skip_preloading_value
+ end
+
private
def relation
- @relation ||= Relation.new(FakeKlass, Post.arel_table, Post.predicate_builder)
+ @relation ||= Relation.new(FakeKlass)
end
end
end
diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb
index 7e418f9c7d..065819e0f1 100644
--- a/activerecord/test/cases/relation/or_test.rb
+++ b/activerecord/test/cases/relation/or_test.rb
@@ -126,5 +126,12 @@ module ActiveRecord
expected = Author.find(1).posts + Post.where(title: "I don't have any comments")
assert_equal expected.sort_by(&:id), actual.sort_by(&:id)
end
+
+ def test_or_with_scope_on_association
+ author = Author.first
+ assert_nothing_raised do
+ author.top_posts.or(author.other_top_posts)
+ end
+ end
end
end
diff --git a/activerecord/test/cases/relation/select_test.rb b/activerecord/test/cases/relation/select_test.rb
new file mode 100644
index 0000000000..0577e6bfdb
--- /dev/null
+++ b/activerecord/test/cases/relation/select_test.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+module ActiveRecord
+ class SelectTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def test_select_with_nil_agrument
+ expected = Post.select(:title).to_sql
+ assert_equal expected, Post.select(nil).select(:title).to_sql
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb
index e5eb159d36..8703d238a0 100644
--- a/activerecord/test/cases/relation/where_clause_test.rb
+++ b/activerecord/test/cases/relation/where_clause_test.rb
@@ -75,8 +75,8 @@ class ActiveRecord::Relation
end
test "a clause knows if it is empty" do
- assert WhereClause.empty.empty?
- assert_not WhereClause.new(["anything"]).empty?
+ assert_empty WhereClause.empty
+ assert_not_empty WhereClause.new(["anything"])
end
test "invert cannot handle nil" do
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index d95a54a2fe..99797528b2 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -265,7 +265,7 @@ module ActiveRecord
end
def test_where_with_decimal_for_string_column
- count = Post.where(title: BigDecimal.new(0)).count
+ count = Post.where(title: BigDecimal(0)).count
assert_equal 0, count
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index fd5985ffe7..0f446e06aa 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -11,30 +11,29 @@ module ActiveRecord
fixtures :posts, :comments, :authors, :author_addresses, :ratings
def test_construction
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass, table: :b)
assert_equal FakeKlass, relation.klass
assert_equal :b, relation.table
assert !relation.loaded, "relation is not loaded"
end
def test_responds_to_model_and_returns_klass
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal FakeKlass, relation.model
end
def test_initialize_single_values
- relation = Relation.new(FakeKlass, :b, nil)
- (Relation::SINGLE_VALUE_METHODS - [:create_with, :readonly]).each do |method|
+ relation = Relation.new(FakeKlass)
+ (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method|
assert_nil relation.send("#{method}_value"), method.to_s
end
- assert_equal false, relation.readonly_value
value = relation.create_with_value
assert_equal({}, value)
assert_predicate value, :frozen?
end
def test_multi_value_initialize
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
Relation::MULTI_VALUE_METHODS.each do |method|
values = relation.send("#{method}_values")
assert_equal [], values, method.to_s
@@ -43,49 +42,49 @@ module ActiveRecord
end
def test_extensions
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal [], relation.extensions
end
def test_empty_where_values_hash
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal({}, relation.where_values_hash)
end
def test_has_values
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
relation.where!(id: 10)
assert_equal({ "id" => 10 }, relation.where_values_hash)
end
def test_values_wrong_table
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
relation.where! Comment.arel_table[:id].eq(10)
assert_equal({}, relation.where_values_hash)
end
def test_tree_is_not_traversed
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
left = relation.table[:id].eq(10)
right = relation.table[:id].eq(10)
- combine = left.and right
+ combine = left.or(right)
relation.where! combine
assert_equal({}, relation.where_values_hash)
end
def test_scope_for_create
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal({}, relation.scope_for_create)
end
def test_create_with_value
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
relation.create_with_value = { hello: "world" }
assert_equal({ "hello" => "world" }, relation.scope_for_create)
end
def test_create_with_value_with_wheres
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
assert_equal({}, relation.scope_for_create)
relation.where!(id: 10)
@@ -96,11 +95,11 @@ module ActiveRecord
end
def test_empty_scope
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
- assert relation.empty_scope?
+ relation = Relation.new(Post)
+ assert_predicate relation, :empty_scope?
relation.merge!(relation)
- assert relation.empty_scope?
+ assert_predicate relation, :empty_scope?
end
def test_bad_constants_raise_errors
@@ -110,31 +109,31 @@ module ActiveRecord
end
def test_empty_eager_loading?
- relation = Relation.new(FakeKlass, :b, nil)
- assert !relation.eager_loading?
+ relation = Relation.new(FakeKlass)
+ assert_not_predicate relation, :eager_loading?
end
def test_eager_load_values
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
relation.eager_load! :b
- assert relation.eager_loading?
+ assert_predicate relation, :eager_loading?
end
def test_references_values
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal [], relation.references_values
relation = relation.references(:foo).references(:omg, :lol)
assert_equal ["foo", "omg", "lol"], relation.references_values
end
def test_references_values_dont_duplicate
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
relation = relation.references(:foo).references(:foo)
assert_equal ["foo"], relation.references_values
end
test "merging a hash into a relation" do
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
relation = relation.merge where: { name: :lol }, readonly: true
assert_equal({ "name" => :lol }, relation.where_clause.to_h)
@@ -142,7 +141,7 @@ module ActiveRecord
end
test "merging an empty hash into a relation" do
- assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass, :b, nil).merge({}).where_clause
+ assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass).merge({}).where_clause
end
test "merging a hash with unknown keys raises" do
@@ -150,7 +149,7 @@ module ActiveRecord
end
test "merging nil or false raises" do
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
e = assert_raises(ArgumentError) do
relation = relation.merge nil
@@ -166,7 +165,7 @@ module ActiveRecord
end
test "#values returns a dup of the values" do
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder).where!(name: :foo)
+ relation = Relation.new(Post).where!(name: :foo)
values = relation.values
values[:where] = nil
@@ -174,7 +173,7 @@ module ActiveRecord
end
test "relations can be created with a values hash" do
- relation = Relation.new(FakeKlass, :b, nil, select: [:foo])
+ relation = Relation.new(FakeKlass, values: { select: [:foo] })
assert_equal [:foo], relation.select_values
end
@@ -186,13 +185,13 @@ module ActiveRecord
end
end
- relation = Relation.new(klass, :b, nil)
+ relation = Relation.new(klass)
relation.merge!(where: ["foo = ?", "bar"])
assert_equal Relation::WhereClause.new(["foo = bar"]), relation.where_clause
end
def test_merging_readonly_false
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
readonly_false_relation = relation.readonly(false)
# test merging in both directions
assert_equal false, relation.merge(readonly_false_relation).readonly_value
@@ -236,17 +235,17 @@ module ActiveRecord
def test_merge_raises_with_invalid_argument
assert_raises ArgumentError do
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
relation.merge(true)
end
end
def test_respond_to_for_non_selected_element
post = Post.select(:title).first
- assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception"
+ assert_not_respond_to post, :body, "post should not respond_to?(:body) since invoking it raises exception"
silence_warnings { post = Post.select("'title' as post_title").first }
- assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception"
+ assert_not_respond_to post, :title, "post should not respond_to?(:body) since invoking it raises exception"
end
def test_select_quotes_when_using_from_clause
@@ -274,18 +273,32 @@ module ActiveRecord
assert_equal({ 2 => 1, 4 => 3, 5 => 1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count)
end
+ def test_relation_merging_keeps_joining_order
+ authors = Author.where(id: 1)
+ posts = Post.joins(:author).merge(authors)
+ comments = Comment.joins(:post).merge(posts)
+ ratings = Rating.joins(:comment).merge(comments)
+
+ assert_equal 3, ratings.count
+ end
+
class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value
def type
:string
end
+ def cast(value)
+ raise value unless value == "value from user"
+ "cast value"
+ end
+
def deserialize(value)
raise value unless value == "type cast for database"
"type cast from database"
end
def serialize(value)
- raise value unless value == "value from user"
+ raise value unless value == "cast value"
"type cast for database"
end
end
@@ -302,6 +315,14 @@ module ActiveRecord
assert_equal "type cast from database", UpdateAllTestModel.first.body
end
+ def test_skip_preloading_after_arel_has_been_generated
+ assert_nothing_raised do
+ relation = Comment.all
+ relation.arel
+ relation.skip_preloading!
+ end
+ end
+
private
def skip_if_sqlite3_version_includes_quoting_bug
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index ae1dc35bff..952d2dd5d9 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -9,6 +9,7 @@ require "models/comment"
require "models/author"
require "models/entrant"
require "models/developer"
+require "models/person"
require "models/computer"
require "models/reply"
require "models/company"
@@ -19,12 +20,13 @@ require "models/tyre"
require "models/minivan"
require "models/possession"
require "models/reader"
+require "models/category"
require "models/categorization"
require "models/edge"
+require "models/subscriber"
class RelationTest < ActiveRecord::TestCase
- fixtures :authors, :author_addresses, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments,
- :tags, :taggings, :cars, :minivans
+ fixtures :authors, :author_addresses, :topics, :entrants, :developers, :people, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans
class TopicWithCallbacks < ActiveRecord::Base
self.table_name = :topics
@@ -56,7 +58,7 @@ class RelationTest < ActiveRecord::TestCase
def test_dynamic_finder
x = Post.where("author_id = ?", 1)
- assert x.klass.respond_to?(:find_by_id), "@klass should handle dynamic finders"
+ assert_respond_to x.klass, :find_by_id
end
def test_multivalue_where
@@ -98,7 +100,7 @@ class RelationTest < ActiveRecord::TestCase
2.times { assert_equal 5, topics.to_a.size }
end
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
def test_scoped_first
@@ -108,7 +110,7 @@ class RelationTest < ActiveRecord::TestCase
2.times { assert_equal "The First Topic", topics.first.title }
end
- assert ! topics.loaded?
+ assert_not_predicate topics, :loaded?
end
def test_loaded_first
@@ -119,7 +121,7 @@ class RelationTest < ActiveRecord::TestCase
assert_equal "The First Topic", topics.first.title
end
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
def test_loaded_first_with_limit
@@ -131,7 +133,7 @@ class RelationTest < ActiveRecord::TestCase
"The Second Topic of the day"], topics.first(2).map(&:title)
end
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
def test_first_get_more_than_available
@@ -152,14 +154,14 @@ class RelationTest < ActiveRecord::TestCase
2.times { topics.to_a }
end
- assert topics.loaded?
+ assert_predicate topics, :loaded?
original_size = topics.to_a.size
Topic.create! title: "fake"
assert_queries(1) { topics.reload }
assert_equal original_size + 1, topics.size
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
def test_finding_with_subquery
@@ -195,6 +197,18 @@ class RelationTest < ActiveRecord::TestCase
assert_equal(relation.map(&:post_count).sort, subquery.values.sort)
end
+ def test_finding_with_subquery_with_eager_loading_in_from
+ relation = Comment.includes(:post).where("posts.type": "Post")
+ assert_equal relation.to_a, Comment.select("*").from(relation).to_a
+ assert_equal relation.to_a, Comment.select("subquery.*").from(relation).to_a
+ assert_equal relation.to_a, Comment.select("a.*").from(relation, :a).to_a
+ end
+
+ def test_finding_with_subquery_with_eager_loading_in_where
+ relation = Comment.includes(:post).where("posts.type": "Post")
+ assert_equal relation.sort_by(&:id), Comment.where(id: relation).sort_by(&:id)
+ end
+
def test_finding_with_conditions
assert_equal ["David"], Author.where(name: "David").map(&:name)
assert_equal ["Mary"], Author.where(["name = ?", "Mary"]).map(&:name)
@@ -238,7 +252,7 @@ class RelationTest < ActiveRecord::TestCase
end
def test_reverse_order_with_function
- topics = Topic.order("length(title)").reverse_order
+ topics = Topic.order(Arel.sql("length(title)")).reverse_order
assert_equal topics(:second).title, topics.first.title
end
@@ -248,24 +262,24 @@ class RelationTest < ActiveRecord::TestCase
end
def test_reverse_order_with_function_other_predicates
- topics = Topic.order("author_name, length(title), id").reverse_order
+ topics = Topic.order(Arel.sql("author_name, length(title), id")).reverse_order
assert_equal topics(:second).title, topics.first.title
- topics = Topic.order("length(author_name), id, length(title)").reverse_order
+ topics = Topic.order(Arel.sql("length(author_name), id, length(title)")).reverse_order
assert_equal topics(:fifth).title, topics.first.title
end
def test_reverse_order_with_multiargument_function
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("concat(author_name, title)").reverse_order
+ Topic.order(Arel.sql("concat(author_name, title)")).reverse_order
end
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("concat(lower(author_name), title)").reverse_order
+ Topic.order(Arel.sql("concat(lower(author_name), title)")).reverse_order
end
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("concat(author_name, lower(title))").reverse_order
+ Topic.order(Arel.sql("concat(author_name, lower(title))")).reverse_order
end
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("concat(lower(author_name), title, length(title)").reverse_order
+ Topic.order(Arel.sql("concat(lower(author_name), title, length(title)")).reverse_order
end
end
@@ -277,10 +291,10 @@ class RelationTest < ActiveRecord::TestCase
def test_reverse_order_with_nulls_first_or_last
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("title NULLS FIRST").reverse_order
+ Topic.order(Arel.sql("title NULLS FIRST")).reverse_order
end
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("title nulls last").reverse_order
+ Topic.order(Arel.sql("title nulls last")).reverse_order
end
end
@@ -373,29 +387,29 @@ class RelationTest < ActiveRecord::TestCase
def test_finding_with_cross_table_order_and_limit
tags = Tag.includes(:taggings).
- order("tags.name asc", "taggings.taggable_id asc", "REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").
+ order("tags.name asc", "taggings.taggable_id asc", Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).
limit(1).to_a
assert_equal 1, tags.length
end
def test_finding_with_complex_order_and_limit
- tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").limit(1).to_a
+ tags = Tag.includes(:taggings).references(:taggings).order(Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).limit(1).to_a
assert_equal 1, tags.length
end
def test_finding_with_complex_order
- tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a
+ tags = Tag.includes(:taggings).references(:taggings).order(Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).to_a
assert_equal 3, tags.length
end
def test_finding_with_sanitized_order
- query = Tag.order(["field(id, ?)", [1, 3, 2]]).to_sql
+ query = Tag.order([Arel.sql("field(id, ?)"), [1, 3, 2]]).to_sql
assert_match(/field\(id, 1,3,2\)/, query)
- query = Tag.order(["field(id, ?)", []]).to_sql
+ query = Tag.order([Arel.sql("field(id, ?)"), []]).to_sql
assert_match(/field\(id, NULL\)/, query)
- query = Tag.order(["field(id, ?)", nil]).to_sql
+ query = Tag.order([Arel.sql("field(id, ?)"), nil]).to_sql
assert_match(/field\(id, NULL\)/, query)
end
@@ -489,16 +503,16 @@ class RelationTest < ActiveRecord::TestCase
relation = Topic.all
["find_by_title", "find_by_title_and_author_name"].each do |method|
- assert_respond_to relation, method, "Topic.all should respond to #{method.inspect}"
+ assert_respond_to relation, method
end
end
def test_respond_to_class_methods_and_scopes
- assert Topic.all.respond_to?(:by_lifo)
+ assert_respond_to Topic.all, :by_lifo
end
def test_find_with_readonly_option
- Developer.all.each { |d| assert !d.readonly? }
+ Developer.all.each { |d| assert_not d.readonly? }
Developer.all.readonly.each { |d| assert d.readonly? }
end
@@ -589,7 +603,7 @@ class RelationTest < ActiveRecord::TestCase
reader = Reader.create! post_id: post.id, person_id: 1
comment = Comment.create! post_id: post.id, body: "body"
- assert !comment.respond_to?(:readers)
+ assert_not_respond_to comment, :readers
post_rel = Post.preload(:readers).joins(:readers).where(title: "Uhuu")
result_comment = Comment.joins(:post).merge(post_rel).to_a.first
@@ -710,7 +724,7 @@ class RelationTest < ActiveRecord::TestCase
def test_find_in_empty_array
authors = Author.all.where(id: [])
- assert authors.to_a.blank?
+ assert_predicate authors.to_a, :blank?
end
def test_where_with_ar_object
@@ -768,8 +782,6 @@ class RelationTest < ActiveRecord::TestCase
def test_find_all_using_where_with_relation
david = authors(:david)
- # switching the lines below would succeed in current rails
- # assert_queries(2) {
assert_queries(1) {
relation = Author.where(id: Author.where(id: david.id))
assert_equal [david], relation.to_a
@@ -808,8 +820,6 @@ class RelationTest < ActiveRecord::TestCase
def test_find_all_using_where_with_relation_and_alternate_primary_key
cool_first = minivans(:cool_first)
- # switching the lines below would succeed in current rails
- # assert_queries(2) {
assert_queries(1) {
relation = Minivan.where(minivan_id: Minivan.where(name: cool_first.name))
assert_equal [cool_first], relation.to_a
@@ -845,32 +855,6 @@ class RelationTest < ActiveRecord::TestCase
}
end
- def test_exists
- davids = Author.where(name: "David")
- assert davids.exists?
- assert davids.exists?(authors(:david).id)
- assert ! davids.exists?(authors(:mary).id)
- assert ! davids.exists?("42")
- assert ! davids.exists?(42)
- assert ! davids.exists?(davids.new.id)
-
- fake = Author.where(name: "fake author")
- assert ! fake.exists?
- assert ! fake.exists?(authors(:david).id)
- end
-
- def test_exists_uses_existing_scope
- post = authors(:david).posts.first
- authors = Author.includes(:posts).where(name: "David", posts: { id: post.id })
- assert authors.exists?(authors(:david).id)
- end
-
- def test_any_with_scope_on_hash_includes
- post = authors(:david).posts.first
- categories = Categorization.includes(author: :posts).where(posts: { id: post.id })
- assert categories.exists?
- end
-
def test_last
authors = Author.all
assert_equal authors(:bob), authors.last
@@ -881,19 +865,19 @@ class RelationTest < ActiveRecord::TestCase
# Force load
assert_equal [authors(:david)], davids.to_a
- assert davids.loaded?
+ assert_predicate davids, :loaded?
assert_difference("Author.count", -1) { davids.destroy_all }
assert_equal [], davids.to_a
- assert davids.loaded?
+ assert_predicate davids, :loaded?
end
def test_delete_all
davids = Author.where(name: "David")
assert_difference("Author.count", -1) { davids.delete_all }
- assert ! davids.loaded?
+ assert_not_predicate davids, :loaded?
end
def test_delete_all_loaded
@@ -901,20 +885,18 @@ class RelationTest < ActiveRecord::TestCase
# Force load
assert_equal [authors(:david)], davids.to_a
- assert davids.loaded?
+ assert_predicate davids, :loaded?
assert_difference("Author.count", -1) { davids.delete_all }
assert_equal [], davids.to_a
- assert davids.loaded?
+ assert_predicate davids, :loaded?
end
def test_delete_all_with_unpermitted_relation_raises_error
- assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all }
assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all }
assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all }
assert_raises(ActiveRecord::ActiveRecordError) { Author.having("SUM(id) < 3").delete_all }
- assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(10).delete_all }
end
def test_select_with_aggregates
@@ -922,9 +904,9 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 11, posts.count(:all)
assert_equal 11, posts.size
- assert posts.any?
- assert posts.many?
- assert_not posts.empty?
+ assert_predicate posts, :any?
+ assert_predicate posts, :many?
+ assert_not_empty posts
end
def test_select_takes_a_variable_list_of_args
@@ -953,13 +935,13 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 11, posts.count(:all)
assert_equal 11, posts.count(:id)
- assert_equal 1, posts.where("comments_count > 1").count
- assert_equal 9, posts.where(comments_count: 0).count
+ assert_equal 3, posts.where("comments_count > 1").count
+ assert_equal 6, posts.where(comments_count: 0).count
end
def test_count_with_block
posts = Post.all
- assert_equal 10, posts.count { |p| p.comments_count.even? }
+ assert_equal 8, posts.count { |p| p.comments_count.even? }
end
def test_count_on_association_relation
@@ -970,19 +952,37 @@ class RelationTest < ActiveRecord::TestCase
assert_equal author.posts.where(author_id: author.id).size, posts.count
assert_equal 0, author.posts.where(author_id: another_author.id).size
- assert author.posts.where(author_id: another_author.id).empty?
+ assert_empty author.posts.where(author_id: another_author.id)
end
def test_count_with_distinct
posts = Post.all
- assert_equal 3, posts.distinct(true).count(:comments_count)
+ assert_equal 4, posts.distinct(true).count(:comments_count)
assert_equal 11, posts.distinct(false).count(:comments_count)
- assert_equal 3, posts.distinct(true).select(:comments_count).count
+ assert_equal 4, posts.distinct(true).select(:comments_count).count
assert_equal 11, posts.distinct(false).select(:comments_count).count
end
+ def test_size_with_distinct
+ posts = Post.distinct.select(:author_id, :comments_count)
+ assert_queries(1) { assert_equal 8, posts.size }
+ assert_queries(1) { assert_equal 8, posts.load.size }
+ end
+
+ def test_size_with_eager_loading_and_custom_order
+ posts = Post.includes(:comments).order("comments.id")
+ assert_queries(1) { assert_equal 11, posts.size }
+ assert_queries(1) { assert_equal 11, posts.load.size }
+ end
+
+ def test_size_with_eager_loading_and_custom_order_and_distinct
+ posts = Post.includes(:comments).order("comments.id").distinct
+ assert_queries(1) { assert_equal 11, posts.size }
+ assert_queries(1) { assert_equal 11, posts.load.size }
+ end
+
def test_update_all_with_scope
tag = Tag.first
Post.tagged_with(tag.id).update_all title: "rofl"
@@ -1014,29 +1014,29 @@ class RelationTest < ActiveRecord::TestCase
posts = Post.all
assert_queries(1) { assert_equal 11, posts.size }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
best_posts = posts.where(comments_count: 0)
best_posts.load # force load
- assert_no_queries { assert_equal 9, best_posts.size }
+ assert_no_queries { assert_equal 6, best_posts.size }
end
def test_size_with_limit
posts = Post.limit(10)
assert_queries(1) { assert_equal 10, posts.size }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
best_posts = posts.where(comments_count: 0)
best_posts.load # force load
- assert_no_queries { assert_equal 9, best_posts.size }
+ assert_no_queries { assert_equal 6, best_posts.size }
end
def test_size_with_zero_limit
posts = Post.limit(0)
assert_no_queries { assert_equal 0, posts.size }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
posts.load # force load
assert_no_queries { assert_equal 0, posts.size }
@@ -1046,13 +1046,13 @@ class RelationTest < ActiveRecord::TestCase
posts = Post.limit(0)
assert_no_queries { assert_equal true, posts.empty? }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
end
def test_count_complex_chained_relations
posts = Post.select("comments_count").where("id is not null").group("author_id").where("comments_count > 0")
- expected = { 1 => 2 }
+ expected = { 1 => 4, 2 => 1 }
assert_equal expected, posts.count
end
@@ -1060,11 +1060,11 @@ class RelationTest < ActiveRecord::TestCase
posts = Post.all
assert_queries(1) { assert_equal false, posts.empty? }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
no_posts = posts.where(title: "")
assert_queries(1) { assert_equal true, no_posts.empty? }
- assert ! no_posts.loaded?
+ assert_not_predicate no_posts, :loaded?
best_posts = posts.where(comments_count: 0)
best_posts.load # force load
@@ -1075,11 +1075,11 @@ class RelationTest < ActiveRecord::TestCase
posts = Post.select("comments_count").where("id is not null").group("author_id").where("comments_count > 0")
assert_queries(1) { assert_equal false, posts.empty? }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
no_posts = posts.where(title: "")
assert_queries(1) { assert_equal true, no_posts.empty? }
- assert ! no_posts.loaded?
+ assert_not_predicate no_posts, :loaded?
end
def test_any
@@ -1095,13 +1095,13 @@ class RelationTest < ActiveRecord::TestCase
assert_queries(3) do
assert posts.any? # Uses COUNT()
- assert ! posts.where(id: nil).any?
+ assert_not_predicate posts.where(id: nil), :any?
assert posts.any? { |p| p.id > 0 }
- assert ! posts.any? { |p| p.id <= 0 }
+ assert_not posts.any? { |p| p.id <= 0 }
end
- assert posts.loaded?
+ assert_predicate posts, :loaded?
end
def test_many
@@ -1110,49 +1110,49 @@ class RelationTest < ActiveRecord::TestCase
assert_queries(2) do
assert posts.many? # Uses COUNT()
assert posts.many? { |p| p.id > 0 }
- assert ! posts.many? { |p| p.id < 2 }
+ assert_not posts.many? { |p| p.id < 2 }
end
- assert posts.loaded?
+ assert_predicate posts, :loaded?
end
def test_many_with_limits
posts = Post.all
- assert posts.many?
- assert ! posts.limit(1).many?
+ assert_predicate posts, :many?
+ assert_not_predicate posts.limit(1), :many?
end
def test_none?
posts = Post.all
assert_queries(1) do
- assert ! posts.none? # Uses COUNT()
+ assert_not posts.none? # Uses COUNT()
end
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
assert_queries(1) do
assert posts.none? { |p| p.id < 0 }
- assert ! posts.none? { |p| p.id == 1 }
+ assert_not posts.none? { |p| p.id == 1 }
end
- assert posts.loaded?
+ assert_predicate posts, :loaded?
end
def test_one
posts = Post.all
assert_queries(1) do
- assert ! posts.one? # Uses COUNT()
+ assert_not posts.one? # Uses COUNT()
end
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
assert_queries(1) do
- assert ! posts.one? { |p| p.id < 3 }
+ assert_not posts.one? { |p| p.id < 3 }
assert posts.one? { |p| p.id == 1 }
end
- assert posts.loaded?
+ assert_predicate posts, :loaded?
end
def test_to_a_should_dup_target
@@ -1185,10 +1185,10 @@ class RelationTest < ActiveRecord::TestCase
sparrow = birds.create
assert_kind_of Bird, sparrow
- assert !sparrow.persisted?
+ assert_not_predicate sparrow, :persisted?
hen = birds.where(name: "hen").create
- assert hen.persisted?
+ assert_predicate hen, :persisted?
assert_equal "hen", hen.name
end
@@ -1199,34 +1199,43 @@ class RelationTest < ActiveRecord::TestCase
hen = birds.where(name: "hen").create!
assert_kind_of Bird, hen
- assert hen.persisted?
+ assert_predicate hen, :persisted?
assert_equal "hen", hen.name
end
+ def test_create_with_polymorphic_association
+ author = authors(:david)
+ post = posts(:welcome)
+ comment = Comment.where(post: post, author: author).create!(body: "hello")
+
+ assert_equal author, comment.author
+ assert_equal post, comment.post
+ end
+
def test_first_or_create
parrot = Bird.where(color: "green").first_or_create(name: "parrot")
assert_kind_of Bird, parrot
- assert parrot.persisted?
+ assert_predicate parrot, :persisted?
assert_equal "parrot", parrot.name
assert_equal "green", parrot.color
same_parrot = Bird.where(color: "green").first_or_create(name: "parakeet")
assert_kind_of Bird, same_parrot
- assert same_parrot.persisted?
+ assert_predicate same_parrot, :persisted?
assert_equal parrot, same_parrot
end
def test_first_or_create_with_no_parameters
parrot = Bird.where(color: "green").first_or_create
assert_kind_of Bird, parrot
- assert !parrot.persisted?
+ assert_not_predicate parrot, :persisted?
assert_equal "green", parrot.color
end
def test_first_or_create_with_block
parrot = Bird.where(color: "green").first_or_create { |bird| bird.name = "parrot" }
assert_kind_of Bird, parrot
- assert parrot.persisted?
+ assert_predicate parrot, :persisted?
assert_equal "green", parrot.color
assert_equal "parrot", parrot.name
@@ -1247,13 +1256,13 @@ class RelationTest < ActiveRecord::TestCase
def test_first_or_create_bang_with_valid_options
parrot = Bird.where(color: "green").first_or_create!(name: "parrot")
assert_kind_of Bird, parrot
- assert parrot.persisted?
+ assert_predicate parrot, :persisted?
assert_equal "parrot", parrot.name
assert_equal "green", parrot.color
same_parrot = Bird.where(color: "green").first_or_create!(name: "parakeet")
assert_kind_of Bird, same_parrot
- assert same_parrot.persisted?
+ assert_predicate same_parrot, :persisted?
assert_equal parrot, same_parrot
end
@@ -1268,7 +1277,7 @@ class RelationTest < ActiveRecord::TestCase
def test_first_or_create_bang_with_valid_block
parrot = Bird.where(color: "green").first_or_create! { |bird| bird.name = "parrot" }
assert_kind_of Bird, parrot
- assert parrot.persisted?
+ assert_predicate parrot, :persisted?
assert_equal "green", parrot.color
assert_equal "parrot", parrot.name
@@ -1299,9 +1308,9 @@ class RelationTest < ActiveRecord::TestCase
def test_first_or_initialize
parrot = Bird.where(color: "green").first_or_initialize(name: "parrot")
assert_kind_of Bird, parrot
- assert !parrot.persisted?
- assert parrot.valid?
- assert parrot.new_record?
+ assert_not_predicate parrot, :persisted?
+ assert_predicate parrot, :valid?
+ assert_predicate parrot, :new_record?
assert_equal "parrot", parrot.name
assert_equal "green", parrot.color
end
@@ -1309,18 +1318,18 @@ class RelationTest < ActiveRecord::TestCase
def test_first_or_initialize_with_no_parameters
parrot = Bird.where(color: "green").first_or_initialize
assert_kind_of Bird, parrot
- assert !parrot.persisted?
- assert !parrot.valid?
- assert parrot.new_record?
+ assert_not_predicate parrot, :persisted?
+ assert_not_predicate parrot, :valid?
+ assert_predicate parrot, :new_record?
assert_equal "green", parrot.color
end
def test_first_or_initialize_with_block
parrot = Bird.where(color: "green").first_or_initialize { |bird| bird.name = "parrot" }
assert_kind_of Bird, parrot
- assert !parrot.persisted?
- assert parrot.valid?
- assert parrot.new_record?
+ assert_not_predicate parrot, :persisted?
+ assert_predicate parrot, :valid?
+ assert_predicate parrot, :new_record?
assert_equal "green", parrot.color
assert_equal "parrot", parrot.name
end
@@ -1329,7 +1338,7 @@ class RelationTest < ActiveRecord::TestCase
assert_nil Bird.find_by(name: "bob")
bird = Bird.find_or_create_by(name: "bob")
- assert bird.persisted?
+ assert_predicate bird, :persisted?
assert_equal bird, Bird.find_or_create_by(name: "bob")
end
@@ -1338,7 +1347,7 @@ class RelationTest < ActiveRecord::TestCase
assert_nil Bird.find_by(name: "bob")
bird = Bird.create_with(color: "green").find_or_create_by(name: "bob")
- assert bird.persisted?
+ assert_predicate bird, :persisted?
assert_equal "green", bird.color
assert_equal bird, Bird.create_with(color: "blue").find_or_create_by(name: "bob")
@@ -1348,11 +1357,39 @@ class RelationTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: "green") }
end
+ def test_create_or_find_by
+ assert_nil Subscriber.find_by(nick: "bob")
+
+ subscriber = Subscriber.create!(nick: "bob")
+
+ assert_equal subscriber, Subscriber.create_or_find_by(nick: "bob")
+ assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat")
+ end
+
+ def test_create_or_find_by_with_non_unique_attributes
+ Subscriber.create!(nick: "bob", name: "the builder")
+
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Subscriber.create_or_find_by(nick: "bob", name: "the cat")
+ end
+ end
+
+ def test_create_or_find_by_within_transaction
+ assert_nil Subscriber.find_by(nick: "bob")
+
+ subscriber = Subscriber.create!(nick: "bob")
+
+ Subscriber.transaction do
+ assert_equal subscriber, Subscriber.create_or_find_by(nick: "bob")
+ assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat")
+ end
+ end
+
def test_find_or_initialize_by
assert_nil Bird.find_by(name: "bob")
bird = Bird.find_or_initialize_by(name: "bob")
- assert bird.new_record?
+ assert_predicate bird, :new_record?
bird.save!
assert_equal bird, Bird.find_or_initialize_by(name: "bob")
@@ -1489,6 +1526,50 @@ class RelationTest < ActiveRecord::TestCase
assert_equal posts(:welcome), comments(:greetings).post
end
+ def test_touch_all_updates_records_timestamps
+ david = developers(:david)
+ david_previously_updated_at = david.updated_at
+ jamis = developers(:jamis)
+ jamis_previously_updated_at = jamis.updated_at
+ Developer.where(name: "David").touch_all
+
+ assert_not_equal david_previously_updated_at, david.reload.updated_at
+ assert_equal jamis_previously_updated_at, jamis.reload.updated_at
+ end
+
+ def test_touch_all_with_custom_timestamp
+ developer = developers(:david)
+ previously_created_at = developer.created_at
+ previously_updated_at = developer.updated_at
+ Developer.where(name: "David").touch_all(:created_at)
+ developer = developer.reload
+
+ assert_not_equal previously_created_at, developer.created_at
+ assert_not_equal previously_updated_at, developer.updated_at
+ end
+
+ def test_touch_all_with_given_time
+ developer = developers(:david)
+ previously_created_at = developer.created_at
+ previously_updated_at = developer.updated_at
+ new_time = Time.utc(2015, 2, 16, 4, 54, 0)
+ Developer.where(name: "David").touch_all(:created_at, time: new_time)
+ developer = developer.reload
+
+ assert_not_equal previously_created_at, developer.created_at
+ assert_not_equal previously_updated_at, developer.updated_at
+ assert_equal new_time, developer.created_at
+ assert_equal new_time, developer.updated_at
+ end
+
+ def test_touch_all_updates_locking_column
+ person = people(:david)
+
+ assert_difference -> { person.reload.lock_version }, +1 do
+ Person.where(first_name: "David").touch_all
+ end
+ end
+
def test_update_on_relation
topic1 = TopicWithCallbacks.create! title: "arel", author_name: nil
topic2 = TopicWithCallbacks.create! title: "activerecord", author_name: nil
@@ -1527,10 +1608,10 @@ class RelationTest < ActiveRecord::TestCase
def test_doesnt_add_having_values_if_options_are_blank
scope = Post.having("")
- assert scope.having_clause.empty?
+ assert_empty scope.having_clause
scope = Post.having([])
- assert scope.having_clause.empty?
+ assert_empty scope.having_clause
end
def test_having_with_binds_for_both_where_and_having
@@ -1556,13 +1637,13 @@ class RelationTest < ActiveRecord::TestCase
def test_references_triggers_eager_loading
scope = Post.includes(:comments)
- assert !scope.eager_loading?
- assert scope.references(:comments).eager_loading?
+ assert_not_predicate scope, :eager_loading?
+ assert_predicate scope.references(:comments), :eager_loading?
end
def test_references_doesnt_trigger_eager_loading_if_reference_not_included
scope = Post.references(:comments)
- assert !scope.eager_loading?
+ assert_not_predicate scope, :eager_loading?
end
def test_automatically_added_where_references
@@ -1593,7 +1674,7 @@ class RelationTest < ActiveRecord::TestCase
scope = Post.order("comments.body")
assert_equal ["comments"], scope.references_values
- scope = Post.order("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}")
+ scope = Post.order(Arel.sql("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}"))
if current_adapter?(:OracleAdapter)
assert_equal ["COMMENTS"], scope.references_values
else
@@ -1610,7 +1691,7 @@ class RelationTest < ActiveRecord::TestCase
scope = Post.order("comments.body asc")
assert_equal ["comments"], scope.references_values
- scope = Post.order("foo(comments.body)")
+ scope = Post.order(Arel.sql("foo(comments.body)"))
assert_equal [], scope.references_values
end
@@ -1618,7 +1699,7 @@ class RelationTest < ActiveRecord::TestCase
scope = Post.reorder("comments.body")
assert_equal %w(comments), scope.references_values
- scope = Post.reorder("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}")
+ scope = Post.reorder(Arel.sql("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}"))
if current_adapter?(:OracleAdapter)
assert_equal ["COMMENTS"], scope.references_values
else
@@ -1635,7 +1716,7 @@ class RelationTest < ActiveRecord::TestCase
scope = Post.reorder("comments.body asc")
assert_equal %w(comments), scope.references_values
- scope = Post.reorder("foo(comments.body)")
+ scope = Post.reorder(Arel.sql("foo(comments.body)"))
assert_equal [], scope.references_values
end
@@ -1660,7 +1741,7 @@ class RelationTest < ActiveRecord::TestCase
# checking if there are topics is used before you actually display them,
# thus it shouldn't invoke an extra count query.
assert_no_queries { assert topics.present? }
- assert_no_queries { assert !topics.blank? }
+ assert_no_queries { assert_not topics.blank? }
# shows count of topics and loops after loading the query should not trigger extra queries either.
assert_no_queries { topics.size }
@@ -1670,7 +1751,7 @@ class RelationTest < ActiveRecord::TestCase
# count always trigger the COUNT query.
assert_queries(1) { topics.count }
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
test "find_by with hash conditions returns the first matching record" do
@@ -1806,6 +1887,14 @@ class RelationTest < ActiveRecord::TestCase
assert_equal post, custom_post_relation.joins(:author).where!(title: post.title).take
end
+ test "arel_attribute respects a custom table" do
+ assert_equal [posts(:sti_comments)], custom_post_relation.ranked_by_comments.limit_by(1).to_a
+ end
+
+ test "alias_tracker respects a custom table" do
+ assert_equal posts(:welcome), custom_post_relation("categories_posts").joins(:categories).first
+ end
+
test "#load" do
relation = Post.all
assert_queries(1) do
@@ -1837,7 +1926,7 @@ class RelationTest < ActiveRecord::TestCase
test "delegations do not leak to other classes" do
Topic.all.by_lifo
assert Topic.all.class.method_defined?(:by_lifo)
- assert !Post.all.respond_to?(:by_lifo)
+ assert_not_respond_to Post.all, :by_lifo
end
def test_unscope_with_subquery
@@ -1862,7 +1951,7 @@ class RelationTest < ActiveRecord::TestCase
def test_locked_should_not_build_arel
posts = Post.locked
- assert posts.locked?
+ assert_predicate posts, :locked?
assert_nothing_raised { posts.lock!(false) }
end
@@ -1912,12 +2001,29 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ test "#where with set" do
+ david = authors(:david)
+ mary = authors(:mary)
+
+ authors = Author.where(name: ["David", "Mary"].to_set)
+ assert_equal [david, mary], authors
+ end
+
+ test "#where with empty set" do
+ authors = Author.where(name: Set.new)
+ assert_empty authors
+ end
+
private
- def custom_post_relation
- table_alias = Post.arel_table.alias("omg_posts")
+ def custom_post_relation(alias_name = "omg_posts")
+ table_alias = Post.arel_table.alias(alias_name)
table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias)
predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
- ActiveRecord::Relation.create(Post, table_alias, predicate_builder)
+ ActiveRecord::Relation.create(
+ Post,
+ table: table_alias,
+ predicate_builder: predicate_builder
+ )
end
end
diff --git a/activerecord/test/cases/reserved_word_test.rb b/activerecord/test/cases/reserved_word_test.rb
index 0214dbec17..e32605fd11 100644
--- a/activerecord/test/cases/reserved_word_test.rb
+++ b/activerecord/test/cases/reserved_word_test.rb
@@ -39,7 +39,7 @@ class ReservedWordTest < ActiveRecord::TestCase
t.string :order
t.belongs_to :select
end
- @connection.create_table :values, force: true do |t|
+ @connection.create_table :values, primary_key: :as, force: true do |t|
t.belongs_to :group
end
end
@@ -88,6 +88,13 @@ class ReservedWordTest < ActiveRecord::TestCase
assert_equal x, Group.find(x.id)
end
+ def test_delete_all_with_subselect
+ create_test_fixtures :values
+ assert_equal 1, Values.order(:as).limit(1).offset(1).delete_all
+ assert_raise(ActiveRecord::RecordNotFound) { Values.find(2) }
+ assert Values.find(1)
+ end
+
def test_has_one_associations
create_test_fixtures :group, :values
v = Group.find(1).values
@@ -109,7 +116,7 @@ class ReservedWordTest < ActiveRecord::TestCase
end
def test_activerecord_introspection
- assert Group.table_exists?
+ assert_predicate Group, :table_exists?
assert_equal ["id", "order", "select_id"], Group.columns.map(&:name).sort
end
diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb
index 082d663675..778cf86ac3 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -4,6 +4,7 @@ require "cases/helper"
require "models/binary"
require "models/author"
require "models/post"
+require "models/customer"
class SanitizeTest < ActiveRecord::TestCase
def setup
@@ -11,30 +12,30 @@ class SanitizeTest < ActiveRecord::TestCase
def test_sanitize_sql_array_handles_string_interpolation
quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi")
- assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi"])
- assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi".mb_chars])
+ assert_equal "name='#{quoted_bambi}'", Binary.sanitize_sql_array(["name='%s'", "Bambi"])
+ assert_equal "name='#{quoted_bambi}'", Binary.sanitize_sql_array(["name='%s'", "Bambi".mb_chars])
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper")
- assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper"])
- assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper".mb_chars])
+ assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.sanitize_sql_array(["name='%s'", "Bambi\nand\nThumper"])
+ assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.sanitize_sql_array(["name='%s'", "Bambi\nand\nThumper".mb_chars])
end
def test_sanitize_sql_array_handles_bind_variables
quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi"])
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi".mb_chars])
+ assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=?", "Bambi"])
+ assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=?", "Bambi".mb_chars])
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper")
- assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"])
- assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars])
+ assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=?", "Bambi\nand\nThumper"])
+ assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=?", "Bambi\nand\nThumper".mb_chars])
end
def test_sanitize_sql_array_handles_named_bind_variables
quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi"])
- assert_equal "name=#{quoted_bambi} AND id=1", Binary.send(:sanitize_sql_array, ["name=:name AND id=:id", name: "Bambi", id: 1])
+ assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=:name", name: "Bambi"])
+ assert_equal "name=#{quoted_bambi} AND id=1", Binary.sanitize_sql_array(["name=:name AND id=:id", name: "Bambi", id: 1])
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper")
- assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi\nand\nThumper"])
- assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name AND name2=:name", name: "Bambi\nand\nThumper"])
+ assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=:name", name: "Bambi\nand\nThumper"])
+ assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=:name AND name2=:name", name: "Bambi\nand\nThumper"])
end
def test_sanitize_sql_array_handles_relations
@@ -43,42 +44,50 @@ class SanitizeTest < ActiveRecord::TestCase
sub_query_pattern = /\(\bselect\b.*?\bwhere\b.*?\)/i
- select_author_sql = Post.send(:sanitize_sql_array, ["id in (?)", david_posts])
+ select_author_sql = Post.sanitize_sql_array(["id in (?)", david_posts])
assert_match(sub_query_pattern, select_author_sql, "should sanitize `Relation` as subquery for bind variables")
- select_author_sql = Post.send(:sanitize_sql_array, ["id in (:post_ids)", post_ids: david_posts])
+ select_author_sql = Post.sanitize_sql_array(["id in (:post_ids)", post_ids: david_posts])
assert_match(sub_query_pattern, select_author_sql, "should sanitize `Relation` as subquery for named bind variables")
end
def test_sanitize_sql_array_handles_empty_statement
- select_author_sql = Post.send(:sanitize_sql_array, [""])
+ select_author_sql = Post.sanitize_sql_array([""])
assert_equal("", select_author_sql)
end
def test_sanitize_sql_like
- assert_equal '100\%', Binary.send(:sanitize_sql_like, "100%")
- assert_equal 'snake\_cased\_string', Binary.send(:sanitize_sql_like, "snake_cased_string")
- assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint')
- assert_equal "normal string 42", Binary.send(:sanitize_sql_like, "normal string 42")
+ assert_equal '100\%', Binary.sanitize_sql_like("100%")
+ assert_equal 'snake\_cased\_string', Binary.sanitize_sql_like("snake_cased_string")
+ assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.sanitize_sql_like('C:\\Programs\\MsPaint')
+ assert_equal "normal string 42", Binary.sanitize_sql_like("normal string 42")
end
def test_sanitize_sql_like_with_custom_escape_character
- assert_equal "100!%", Binary.send(:sanitize_sql_like, "100%", "!")
- assert_equal "snake!_cased!_string", Binary.send(:sanitize_sql_like, "snake_cased_string", "!")
- assert_equal "great!!", Binary.send(:sanitize_sql_like, "great!", "!")
- assert_equal 'C:\\Programs\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint', "!")
- assert_equal "normal string 42", Binary.send(:sanitize_sql_like, "normal string 42", "!")
+ assert_equal "100!%", Binary.sanitize_sql_like("100%", "!")
+ assert_equal "snake!_cased!_string", Binary.sanitize_sql_like("snake_cased_string", "!")
+ assert_equal "great!!", Binary.sanitize_sql_like("great!", "!")
+ assert_equal 'C:\\Programs\\MsPaint', Binary.sanitize_sql_like('C:\\Programs\\MsPaint', "!")
+ assert_equal "normal string 42", Binary.sanitize_sql_like("normal string 42", "!")
end
def test_sanitize_sql_like_example_use_case
searchable_post = Class.new(Post) do
- def self.search(term)
+ def self.search_as_method(term)
where("title LIKE ?", sanitize_sql_like(term, "!"))
end
+
+ scope :search_as_scope, -> (term) {
+ where("title LIKE ?", sanitize_sql_like(term, "!"))
+ }
+ end
+
+ assert_sql(/LIKE '20!% !_reduction!_!!'/) do
+ searchable_post.search_as_method("20% _reduction_!").to_a
end
assert_sql(/LIKE '20!% !_reduction!_!!'/) do
- searchable_post.search("20% _reduction_!").to_a
+ searchable_post.search_as_scope("20% _reduction_!").to_a
end
end
@@ -153,24 +162,18 @@ class SanitizeTest < ActiveRecord::TestCase
assert_equal "name=#{quoted_bambi_and_thumper}", bind("name=?", "Bambi\nand\nThumper".mb_chars)
end
- def test_bind_record
- o = Class.new {
- def quoted_id
- 1
- end
- }.new
- assert_deprecated { assert_equal "1", bind("?", o) }
-
- os = [o] * 3
- assert_deprecated { assert_equal "1,1,1", bind("?", os) }
- end
-
def test_named_bind_with_postgresql_type_casts
l = Proc.new { bind(":a::integer '2009-01-01'::date", a: "10") }
assert_nothing_raised(&l)
assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call
end
+ def test_deprecated_expand_hash_conditions_for_aggregates
+ assert_deprecated do
+ assert_equal({ "balance" => 50 }, Customer.send(:expand_hash_conditions_for_aggregates, balance: Money.new(50)))
+ end
+ end
+
private
def bind(statement, *vars)
if vars.first.is_a?(Hash)
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 799a65c61e..31bdf3f357 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -177,14 +177,14 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dumps_index_columns_in_right_order
index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_index/).first.strip
- if current_adapter?(:PostgreSQLAdapter)
- assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition
- elsif current_adapter?(:Mysql2Adapter)
+ if current_adapter?(:Mysql2Adapter)
if ActiveRecord::Base.connection.supports_index_sort_order?
assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }, order: { rating: :desc }', index_definition
else
assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }', index_definition
end
+ elsif ActiveRecord::Base.connection.supports_index_sort_order?
+ assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition
else
assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition
end
@@ -199,6 +199,24 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
end
+ def test_schema_dumps_index_sort_order
+ index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*_name_and_rating/).first.strip
+ if ActiveRecord::Base.connection.supports_index_sort_order?
+ assert_equal 't.index ["name", "rating"], name: "index_companies_on_name_and_rating", order: :desc', index_definition
+ else
+ assert_equal 't.index ["name", "rating"], name: "index_companies_on_name_and_rating"', index_definition
+ end
+ end
+
+ def test_schema_dumps_index_length
+ index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*_name_and_description/).first.strip
+ if current_adapter?(:Mysql2Adapter)
+ assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description", length: 10', index_definition
+ else
+ assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description"', index_definition
+ end
+ end
+
def test_schema_dump_should_honor_nonstandard_primary_keys
output = standard_dump
match = output.match(%r{create_table "movies"(.*)do})
@@ -280,7 +298,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dump_expression_indices
index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip
- assert_equal 't.index "lower((name)::text)", name: "company_expression_index"', index_definition
+ assert_match %r{CASE.+lower\(\(name\)::text\)}i, index_definition
end
def test_schema_dump_interval_type
@@ -294,34 +312,32 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{t\.oid\s+"obj_id"$}, output
end
- if ActiveRecord::Base.connection.supports_extensions?
- def test_schema_dump_includes_extensions
- connection = ActiveRecord::Base.connection
+ def test_schema_dump_includes_extensions
+ connection = ActiveRecord::Base.connection
- connection.stubs(:extensions).returns(["hstore"])
- output = perform_schema_dump
- assert_match "# These are extensions that must be enabled", output
- assert_match %r{enable_extension "hstore"}, output
+ connection.stubs(:extensions).returns(["hstore"])
+ output = perform_schema_dump
+ assert_match "# These are extensions that must be enabled", output
+ assert_match %r{enable_extension "hstore"}, output
- connection.stubs(:extensions).returns([])
- output = perform_schema_dump
- assert_no_match "# These are extensions that must be enabled", output
- assert_no_match %r{enable_extension}, output
- end
+ connection.stubs(:extensions).returns([])
+ output = perform_schema_dump
+ assert_no_match "# These are extensions that must be enabled", output
+ assert_no_match %r{enable_extension}, output
+ end
- def test_schema_dump_includes_extensions_in_alphabetic_order
- connection = ActiveRecord::Base.connection
+ def test_schema_dump_includes_extensions_in_alphabetic_order
+ connection = ActiveRecord::Base.connection
- connection.stubs(:extensions).returns(["hstore", "uuid-ossp", "xml2"])
- output = perform_schema_dump
- enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten
- assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions
+ connection.stubs(:extensions).returns(["hstore", "uuid-ossp", "xml2"])
+ output = perform_schema_dump
+ enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten
+ assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions
- connection.stubs(:extensions).returns(["uuid-ossp", "xml2", "hstore"])
- output = perform_schema_dump
- enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten
- assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions
- end
+ connection.stubs(:extensions).returns(["uuid-ossp", "xml2", "hstore"])
+ output = perform_schema_dump
+ enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten
+ assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions
end
end
@@ -453,7 +469,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string
assert_match %r{create_table "omg_cats"}, output
- refute_match %r{create_table "cats"}, output
+ assert_no_match %r{create_table "cats"}, output
ensure
migration.migrate(:down)
ActiveRecord::Base.table_name_prefix = original_table_name_prefix
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
index 716ca29eda..e3a34aa50d 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -120,49 +120,49 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_unscope_with_where_attributes
expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.where(name: "David").unscope(where: :name).collect(&:name)
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
expected_2 = Developer.order("salary DESC").collect(&:name)
received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({ where: :name }, :select).collect(&:name)
- assert_equal expected_2, received_2
+ assert_equal expected_2.sort, received_2.sort
expected_3 = Developer.order("salary DESC").collect(&:name)
received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect(&:name)
- assert_equal expected_3, received_3
+ assert_equal expected_3.sort, received_3.sort
expected_4 = Developer.order("salary DESC").collect(&:name)
received_4 = DeveloperOrderedBySalary.where.not("name" => "Jamis").unscope(where: :name).collect(&:name)
- assert_equal expected_4, received_4
+ assert_equal expected_4.sort, received_4.sort
expected_5 = Developer.order("salary DESC").collect(&:name)
received_5 = DeveloperOrderedBySalary.where.not("name" => ["Jamis", "David"]).unscope(where: :name).collect(&:name)
- assert_equal expected_5, received_5
+ assert_equal expected_5.sort, received_5.sort
expected_6 = Developer.order("salary DESC").collect(&:name)
received_6 = DeveloperOrderedBySalary.where(Developer.arel_table["name"].eq("David")).unscope(where: :name).collect(&:name)
- assert_equal expected_6, received_6
+ assert_equal expected_6.sort, received_6.sort
expected_7 = Developer.order("salary DESC").collect(&:name)
received_7 = DeveloperOrderedBySalary.where(Developer.arel_table[:name].eq("David")).unscope(where: :name).collect(&:name)
- assert_equal expected_7, received_7
+ assert_equal expected_7.sort, received_7.sort
end
def test_unscope_comparison_where_clauses
# unscoped for WHERE (`developers`.`id` <= 2)
expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.where(id: -Float::INFINITY..2).unscope(where: :id).collect { |dev| dev.name }
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
# unscoped for WHERE (`developers`.`id` < 2)
expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.where(id: -Float::INFINITY...2).unscope(where: :id).collect { |dev| dev.name }
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
end
def test_unscope_multiple_where_clauses
expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.where(name: "Jamis").where(id: 1).unscope(where: [:name, :id]).collect(&:name)
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
end
def test_unscope_string_where_clauses_involved
@@ -172,28 +172,28 @@ class DefaultScopingTest < ActiveRecord::TestCase
dev_ordered_relation = DeveloperOrderedBySalary.where(name: "Jamis").where("created_at > ?", 1.year.ago)
received = dev_ordered_relation.unscope(where: [:name]).collect(&:name)
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
end
def test_unscope_with_grouping_attributes
expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect(&:name)
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
expected_2 = Developer.order("salary DESC").collect(&:name)
received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect(&:name)
- assert_equal expected_2, received_2
+ assert_equal expected_2.sort, received_2.sort
end
def test_unscope_with_limit_in_query
expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect(&:name)
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
end
def test_order_to_unscope_reordering
scope = DeveloperOrderedBySalary.order("salary DESC, name ASC").reverse_order.unscope(:order)
- assert !/order/i.match?(scope.to_sql)
+ assert_no_match(/order/i, scope.to_sql)
end
def test_unscope_reverse_order
@@ -224,6 +224,18 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal expected, received
end
+ def test_unscope_left_outer_joins
+ expected = Developer.all.collect(&:name)
+ received = Developer.left_outer_joins(:projects).select(:id).unscope(:left_outer_joins, :select).collect(&:name)
+ assert_equal expected, received
+ end
+
+ def test_unscope_left_joins
+ expected = Developer.all.collect(&:name)
+ received = Developer.left_joins(:projects).select(:id).unscope(:left_joins, :select).collect(&:name)
+ assert_equal expected, received
+ end
+
def test_unscope_includes
expected = Developer.all.collect(&:name)
received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect(&:name)
@@ -290,8 +302,8 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_unscope_merging
merged = Developer.where(name: "Jamis").merge(Developer.unscope(:where))
- assert merged.where_clause.empty?
- assert !merged.where(name: "Jon").where_clause.empty?
+ assert_empty merged.where_clause
+ assert_not_empty merged.where(name: "Jon").where_clause
end
def test_order_in_default_scope_should_not_prevail
@@ -472,7 +484,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
test "a scope can remove the condition from the default scope" do
scope = DeveloperCalledJamis.david2
assert_equal 1, scope.where_clause.ast.children.length
- assert_equal Developer.where(name: "David"), scope
+ assert_equal Developer.where(name: "David").map(&:id), scope.map(&:id)
end
def test_with_abstract_class_where_clause_should_not_be_duplicated
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index b0431a4e34..4214f347fb 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -13,7 +13,7 @@ class NamedScopingTest < ActiveRecord::TestCase
fixtures :posts, :authors, :topics, :comments, :author_addresses
def test_implements_enumerable
- assert !Topic.all.empty?
+ assert_not_empty Topic.all
assert_equal Topic.all.to_a, Topic.base
assert_equal Topic.all.to_a, Topic.base.to_a
@@ -40,7 +40,7 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_delegates_finds_and_calculations_to_the_base_class
- assert !Topic.all.empty?
+ assert_not_empty Topic.all
assert_equal Topic.all.to_a, Topic.base.to_a
assert_equal Topic.first, Topic.base.first
@@ -65,13 +65,13 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy
- assert Topic.approved.respond_to?(:limit)
- assert Topic.approved.respond_to?(:count)
- assert Topic.approved.respond_to?(:length)
+ assert_respond_to Topic.approved, :limit
+ assert_respond_to Topic.approved, :count
+ assert_respond_to Topic.approved, :length
end
def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified
- assert !Topic.all.merge!(where: { approved: true }).to_a.empty?
+ assert_not_empty Topic.all.merge!(where: { approved: true }).to_a
assert_equal Topic.all.merge!(where: { approved: true }).to_a, Topic.approved
assert_equal Topic.where(approved: true).count, Topic.approved.count
@@ -86,8 +86,8 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_scopes_are_composable
assert_equal((approved = Topic.all.merge!(where: { approved: true }).to_a), Topic.approved)
assert_equal((replied = Topic.all.merge!(where: "replies_count > 0").to_a), Topic.replied)
- assert !(approved == replied)
- assert !(approved & replied).empty?
+ assert_not (approved == replied)
+ assert_not_empty (approved & replied)
assert_equal approved & replied, Topic.approved.replied
end
@@ -115,7 +115,7 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_has_many_associations_have_access_to_scopes
assert_not_equal Post.containing_the_letter_a, authors(:david).posts
- assert !Post.containing_the_letter_a.empty?
+ assert_not_empty Post.containing_the_letter_a
expected = authors(:david).posts & Post.containing_the_letter_a
assert_equal expected.sort_by(&:id), authors(:david).posts.containing_the_letter_a.sort_by(&:id)
@@ -128,15 +128,15 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_has_many_through_associations_have_access_to_scopes
assert_not_equal Comment.containing_the_letter_e, authors(:david).comments
- assert !Comment.containing_the_letter_e.empty?
+ assert_not_empty Comment.containing_the_letter_e
expected = authors(:david).comments & Comment.containing_the_letter_e
assert_equal expected.sort_by(&:id), authors(:david).comments.containing_the_letter_e.sort_by(&:id)
end
def test_scopes_honor_current_scopes_from_when_defined
- assert !Post.ranked_by_comments.limit_by(5).empty?
- assert !authors(:david).posts.ranked_by_comments.limit_by(5).empty?
+ assert_not_empty Post.ranked_by_comments.limit_by(5)
+ assert_not_empty authors(:david).posts.ranked_by_comments.limit_by(5)
assert_not_equal Post.ranked_by_comments.limit_by(5), authors(:david).posts.ranked_by_comments.limit_by(5)
assert_not_equal Post.top(5), authors(:david).posts.top(5)
# Oracle sometimes sorts differently if WHERE condition is changed
@@ -151,15 +151,31 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal "The scope body needs to be callable.", e.message
end
+ def test_scopes_name_is_relation_method
+ conflicts = [
+ :records,
+ :to_ary,
+ :to_sql,
+ :explain
+ ]
+
+ conflicts.each do |name|
+ e = assert_raises ArgumentError do
+ Class.new(Post).class_eval { scope name, -> { where(approved: true) } }
+ end
+ assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
+ end
+ end
+
def test_active_records_have_scope_named__all__
- assert !Topic.all.empty?
+ assert_not_empty Topic.all
assert_equal Topic.all.to_a, Topic.base
end
def test_active_records_have_scope_named__scoped__
scope = Topic.where("content LIKE '%Have%'")
- assert !scope.empty?
+ assert_not_empty scope
assert_equal scope, Topic.all.merge!(where: "content LIKE '%Have%'")
end
@@ -212,9 +228,9 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_model_class_should_respond_to_any
- assert Topic.any?
+ assert_predicate Topic, :any?
Topic.delete_all
- assert !Topic.any?
+ assert_not_predicate Topic, :any?
end
def test_many_should_not_load_results
@@ -243,22 +259,22 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_many_should_return_false_if_none_or_one
topics = Topic.base.where(id: 0)
- assert !topics.many?
+ assert_not_predicate topics, :many?
topics = Topic.base.where(id: 1)
- assert !topics.many?
+ assert_not_predicate topics, :many?
end
def test_many_should_return_true_if_more_than_one
- assert Topic.base.many?
+ assert_predicate Topic.base, :many?
end
def test_model_class_should_respond_to_many
Topic.delete_all
- assert !Topic.many?
+ assert_not_predicate Topic, :many?
Topic.create!
- assert !Topic.many?
+ assert_not_predicate Topic, :many?
Topic.create!
- assert Topic.many?
+ assert_predicate Topic, :many?
end
def test_should_build_on_top_of_scope
@@ -287,6 +303,13 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal "lifo", topic.author_name
end
+ def test_deprecated_delegating_private_method
+ assert_deprecated do
+ scope = Topic.all.by_private_lifo
+ assert_not scope.instance_variable_get(:@delegate_to_klass)
+ end
+ end
+
def test_reserved_scope_names
klass = Class.new(ActiveRecord::Base) do
self.table_name = "topics"
@@ -407,16 +430,16 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_chaining_applies_last_conditions_when_creating
post = Topic.rejected.new
- assert !post.approved?
+ assert_not_predicate post, :approved?
post = Topic.rejected.approved.new
- assert post.approved?
+ assert_predicate post, :approved?
post = Topic.approved.rejected.new
- assert !post.approved?
+ assert_not_predicate post, :approved?
post = Topic.approved.rejected.approved.new
- assert post.approved?
+ assert_predicate post, :approved?
end
def test_chaining_combines_conditions_when_searching
@@ -465,8 +488,9 @@ class NamedScopingTest < ActiveRecord::TestCase
[:public_method, :protected_method, :private_method].each do |reserved_method|
assert Topic.respond_to?(reserved_method, true)
- ActiveRecord::Base.logger.expects(:warn)
- silence_warnings { Topic.scope(reserved_method, -> {}) }
+ assert_called(ActiveRecord::Base.logger, :warn) do
+ silence_warnings { Topic.scope(reserved_method, -> {}) }
+ end
end
end
@@ -482,7 +506,7 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_index_on_scope
approved = Topic.approved.order("id ASC")
assert_equal topics(:second), approved[0]
- assert approved.loaded?
+ assert_predicate approved, :loaded?
end
def test_nested_scopes_queries_size
@@ -562,16 +586,16 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_model_class_should_respond_to_none
- assert !Topic.none?
+ assert_not_predicate Topic, :none?
Topic.delete_all
- assert Topic.none?
+ assert_predicate Topic, :none?
end
def test_model_class_should_respond_to_one
- assert !Topic.one?
+ assert_not_predicate Topic, :one?
Topic.delete_all
- assert !Topic.one?
+ assert_not_predicate Topic, :one?
Topic.create!
- assert Topic.one?
+ assert_predicate Topic, :one?
end
end
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index f3b84d88c2..f18f1ed981 100644
--- a/activerecord/test/cases/scoping/relation_scoping_test.rb
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -105,7 +105,7 @@ class RelationScopingTest < ActiveRecord::TestCase
Developer.select("id, name").scoping do
developer = Developer.where("name = 'David'").first
assert_equal "David", developer.name
- assert !developer.has_attribute?(:salary)
+ assert_not developer.has_attribute?(:salary)
end
end
@@ -213,21 +213,21 @@ class RelationScopingTest < ActiveRecord::TestCase
def test_current_scope_does_not_pollute_sibling_subclasses
Comment.none.scoping do
- assert_not SpecialComment.all.any?
- assert_not VerySpecialComment.all.any?
- assert_not SubSpecialComment.all.any?
+ assert_not_predicate SpecialComment.all, :any?
+ assert_not_predicate VerySpecialComment.all, :any?
+ assert_not_predicate SubSpecialComment.all, :any?
end
SpecialComment.none.scoping do
- assert Comment.all.any?
- assert VerySpecialComment.all.any?
- assert_not SubSpecialComment.all.any?
+ assert_predicate Comment.all, :any?
+ assert_predicate VerySpecialComment.all, :any?
+ assert_not_predicate SubSpecialComment.all, :any?
end
SubSpecialComment.none.scoping do
- assert Comment.all.any?
- assert VerySpecialComment.all.any?
- assert SpecialComment.all.any?
+ assert_predicate Comment.all, :any?
+ assert_predicate VerySpecialComment.all, :any?
+ assert_predicate SpecialComment.all, :any?
end
end
@@ -240,6 +240,20 @@ class RelationScopingTest < ActiveRecord::TestCase
assert_nil SpecialComment.current_scope
end
+ def test_scoping_respects_current_class
+ Comment.unscoped do
+ assert_equal "a comment...", Comment.all.what_are_you
+ assert_equal "a special comment...", SpecialComment.all.what_are_you
+ end
+ end
+
+ def test_scoping_respects_sti_constraint
+ Comment.unscoped do
+ assert_equal comments(:greetings), Comment.find(1)
+ assert_raises(ActiveRecord::RecordNotFound) { SpecialComment.find(1) }
+ end
+ end
+
def test_circular_joins_with_scoping_does_not_crash
posts = Post.joins(comments: :post).scoping do
Post.first(10)
@@ -320,7 +334,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase
def test_nested_exclusive_scope_for_create
comment = Comment.create_with(body: "Hey guys, nested scopes are broken. Please fix!").scoping do
Comment.unscoped.create_with(post_id: 1).scoping do
- assert Comment.new.body.blank?
+ assert_predicate Comment.new.body, :blank?
Comment.create body: "Hey guys"
end
end
diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
index 2d829ad4ba..932780bfef 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -67,8 +67,8 @@ class SerializationTest < ActiveRecord::TestCase
klazz.include_root_in_json = false
assert ActiveRecord::Base.include_root_in_json
- assert !klazz.include_root_in_json
- assert !klazz.new.include_root_in_json
+ assert_not klazz.include_root_in_json
+ assert_not klazz.new.include_root_in_json
ensure
ActiveRecord::Base.include_root_in_json = original_root_in_json
end
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index 32dafbd458..7de5429cbb 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -279,7 +279,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
topic = Topic.new(content: nil)
- assert_not topic.content_changed?
+ assert_not_predicate topic, :content_changed?
end
def test_classes_without_no_arg_constructors_are_not_supported
@@ -349,7 +349,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
topic = model.create!(foo: "bar")
topic.foo
- refute topic.changed?
+ assert_not_predicate topic, :changed?
end
def test_serialized_attribute_works_under_concurrent_initial_access
diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb
index 1f715e41a6..e3c12f68fd 100644
--- a/activerecord/test/cases/statement_cache_test.rb
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -12,7 +12,6 @@ module ActiveRecord
@connection = ActiveRecord::Base.connection
end
- #Cache v 1.1 tests
def test_statement_cache
Book.create(name: "my book")
Book.create(name: "my other book")
@@ -51,8 +50,6 @@ module ActiveRecord
assert_equal("my other book", b.name)
end
- #End
-
def test_statement_cache_with_simple_statement
cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
Book.where(name: "my book").where("author_id > 3")
@@ -105,7 +102,7 @@ module ActiveRecord
Book.find_by(name: "my other book")
end
- refute_equal book, other_book
+ assert_not_equal book, other_book
end
def test_find_by_does_not_use_statement_cache_if_table_name_is_changed
diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb
index ebf4016960..3bd480cfbd 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -8,7 +8,12 @@ class StoreTest < ActiveRecord::TestCase
fixtures :'admin/users'
setup do
- @john = Admin::User.create!(name: "John Doe", color: "black", remember_login: true, height: "tall", is_a_good_guy: true)
+ @john = Admin::User.create!(
+ name: "John Doe", color: "black", remember_login: true,
+ height: "tall", is_a_good_guy: true,
+ parent_name: "Quinn", partner_name: "Dallas",
+ partner_birthday: "1997-11-1"
+ )
end
test "reading store attributes through accessors" do
@@ -24,6 +29,21 @@ class StoreTest < ActiveRecord::TestCase
assert_equal "37signals.com", @john.homepage
end
+ test "reading store attributes through accessors with prefix" do
+ assert_equal "Quinn", @john.parent_name
+ assert_nil @john.parent_birthday
+ assert_equal "Dallas", @john.partner_name
+ assert_equal "1997-11-1", @john.partner_birthday
+ end
+
+ test "writing store attributes through accessors with prefix" do
+ @john.partner_name = "River"
+ @john.partner_birthday = "1999-2-11"
+
+ assert_equal "River", @john.partner_name
+ assert_equal "1999-2-11", @john.partner_birthday
+ end
+
test "accessing attributes not exposed by accessors" do
@john.settings[:icecream] = "graeters"
@john.save
@@ -45,7 +65,7 @@ class StoreTest < ActiveRecord::TestCase
test "updating the store will mark it as changed" do
@john.color = "red"
- assert @john.settings_changed?
+ assert_predicate @john, :settings_changed?
end
test "updating the store populates the changed array correctly" do
@@ -56,7 +76,7 @@ class StoreTest < ActiveRecord::TestCase
test "updating the store won't mark it as changed if an attribute isn't changed" do
@john.color = @john.color
- assert !@john.settings_changed?
+ assert_not_predicate @john, :settings_changed?
end
test "object initialization with not nullable column" do
@@ -137,7 +157,7 @@ class StoreTest < ActiveRecord::TestCase
test "updating the store will mark it as changed encoded with JSON" do
@john.height = "short"
- assert @john.json_data_changed?
+ assert_predicate @john, :json_data_changed?
end
test "object initialization with not nullable column encoded with JSON" do
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index 1495d2ab89..60c7cb1bb4 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -28,15 +28,32 @@ module ActiveRecord
class DatabaseTasksUtilsTask < ActiveRecord::TestCase
def test_raises_an_error_when_called_with_protected_environment
- ActiveRecord::Migrator.stubs(:current_version).returns(1)
+ ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1)
- protected_environments = ActiveRecord::Base.protected_environments.dup
- current_env = ActiveRecord::Migrator.current_environment
+ protected_environments = ActiveRecord::Base.protected_environments
+ current_env = ActiveRecord::Base.connection.migration_context.current_environment
assert_not_includes protected_environments, current_env
# Assert no error
ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
- ActiveRecord::Base.protected_environments << current_env
+ ActiveRecord::Base.protected_environments = [current_env]
+ assert_raise(ActiveRecord::ProtectedEnvironmentError) do
+ ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+ end
+ ensure
+ ActiveRecord::Base.protected_environments = protected_environments
+ end
+
+ def test_raises_an_error_when_called_with_protected_environment_which_name_is_a_symbol
+ ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1)
+
+ protected_environments = ActiveRecord::Base.protected_environments
+ current_env = ActiveRecord::Base.connection.migration_context.current_environment
+ assert_not_includes protected_environments, current_env
+ # Assert no error
+ ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+
+ ActiveRecord::Base.protected_environments = [current_env.to_sym]
assert_raise(ActiveRecord::ProtectedEnvironmentError) do
ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
end
@@ -46,7 +63,7 @@ module ActiveRecord
def test_raises_an_error_if_no_migrations_have_been_made
ActiveRecord::InternalMetadata.stubs(:table_exists?).returns(false)
- ActiveRecord::Migrator.stubs(:current_version).returns(1)
+ ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1)
assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do
ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
@@ -63,10 +80,11 @@ module ActiveRecord
instance = klazz.new
klazz.stubs(:new).returns instance
- instance.expects(:structure_dump).with("awesome-file.sql", nil)
- ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz)
- ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => :foo }, "awesome-file.sql")
+ assert_called_with(instance, :structure_dump, ["awesome-file.sql", nil]) do
+ ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz)
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => :foo }, "awesome-file.sql")
+ end
end
def test_unregistered_task
@@ -93,6 +111,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.connection, path)
assert File.file?(path)
ensure
+ ActiveRecord::Base.clear_cache!
FileUtils.rm_rf(path)
end
end
@@ -109,50 +128,50 @@ module ActiveRecord
def test_ignores_configurations_without_databases
@configurations["development"].merge!("database" => nil)
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).never
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
end
def test_ignores_remote_databases
@configurations["development"].merge!("host" => "my.server.tld")
$stderr.stubs(:puts).returns(nil)
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).never
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
end
def test_warning_for_remote_databases
@configurations["development"].merge!("host" => "my.server.tld")
- $stderr.expects(:puts).with("This task only modifies local databases. my-db is on a remote host.")
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_called_with($stderr, :puts, ["This task only modifies local databases. my-db is on a remote host."]) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
end
def test_creates_configurations_with_local_ip
@configurations["development"].merge!("host" => "127.0.0.1")
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
end
def test_creates_configurations_with_local_host
@configurations["development"].merge!("host" => "localhost")
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
end
def test_creates_configurations_with_blank_hosts
@configurations["development"].merge!("host" => nil)
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
end
end
@@ -161,7 +180,7 @@ module ActiveRecord
@configurations = {
"development" => { "database" => "dev-db" },
"test" => { "database" => "test-db" },
- "production" => { "database" => "prod-db" }
+ "production" => { "url" => "prod-db-url" }
}
ActiveRecord::Base.stubs(:configurations).returns(@configurations)
@@ -169,8 +188,96 @@ module ActiveRecord
end
def test_creates_current_environment_database
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ ["database" => "test-db"],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+ end
+
+ def test_creates_current_environment_database_with_url
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ ["url" => "prod-db-url"],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
+ end
+
+ def test_creates_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "prod-db")
+ with("database" => "dev-db")
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with("database" => "test-db")
+
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+
+ 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")
+ 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_environments
+ ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true
+
+ ActiveRecord::Base.expects(:establish_connection).with(:development)
+
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+
+ class DatabaseTasksCreateCurrentThreeTierTest < ActiveRecord::TestCase
+ def setup
+ @configurations = {
+ "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } },
+ "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } },
+ "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } }
+ }
+
+ ActiveRecord::Base.stubs(:configurations).returns(@configurations)
+ ActiveRecord::Base.stubs(:establish_connection).returns(true)
+ end
+
+ def test_creates_current_environment_database
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with("database" => "test-db")
+
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with("database" => "secondary-test-db")
+
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+
+ def test_creates_current_environment_database_with_url
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with("url" => "prod-db-url")
+
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with("url" => "secondary-prod-db-url")
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new("production")
@@ -181,7 +288,11 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
with("database" => "dev-db")
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with("database" => "secondary-dev-db")
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
with("database" => "test-db")
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with("database" => "secondary-test-db")
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new("development")
@@ -194,7 +305,11 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
with("database" => "dev-db")
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with("database" => "secondary-dev-db")
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
with("database" => "test-db")
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with("database" => "secondary-test-db")
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new("development")
@@ -203,7 +318,7 @@ module ActiveRecord
ENV["RAILS_ENV"] = old_env
end
- def test_establishes_connection_for_the_given_environment
+ def test_establishes_connection_for_the_given_environments_config
ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true
ActiveRecord::Base.expects(:establish_connection).with(:development)
@@ -235,50 +350,54 @@ module ActiveRecord
def test_ignores_configurations_without_databases
@configurations[:development].merge!("database" => nil)
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
end
def test_ignores_remote_databases
@configurations[:development].merge!("host" => "my.server.tld")
$stderr.stubs(:puts).returns(nil)
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
end
def test_warning_for_remote_databases
@configurations[:development].merge!("host" => "my.server.tld")
- $stderr.expects(:puts).with("This task only modifies local databases. my-db is on a remote host.")
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_called_with(
+ $stderr,
+ :puts,
+ ["This task only modifies local databases. my-db is on a remote host."],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
end
def test_drops_configurations_with_local_ip
@configurations[:development].merge!("host" => "127.0.0.1")
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
end
def test_drops_configurations_with_local_host
@configurations[:development].merge!("host" => "localhost")
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
end
def test_drops_configurations_with_blank_hosts
@configurations[:development].merge!("host" => nil)
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
end
end
@@ -287,7 +406,7 @@ module ActiveRecord
@configurations = {
"development" => { "database" => "dev-db" },
"test" => { "database" => "test-db" },
- "production" => { "database" => "prod-db" }
+ "production" => { "url" => "prod-db-url" }
}
ActiveRecord::Base.stubs(:configurations).returns(@configurations)
@@ -295,7 +414,16 @@ module ActiveRecord
def test_drops_current_environment_database
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "prod-db")
+ with("database" => "test-db")
+
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+
+ def test_drops_current_environment_database_with_url
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("url" => "prod-db-url")
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new("production")
@@ -329,56 +457,208 @@ module ActiveRecord
end
end
- class DatabaseTasksMigrateTest < ActiveRecord::TestCase
- self.use_transactional_tests = false
-
+ class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase
def setup
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths = "custom/path"
+ @configurations = {
+ "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } },
+ "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } },
+ "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } }
+ }
+
+ ActiveRecord::Base.stubs(:configurations).returns(@configurations)
end
- def teardown
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil
+ def test_drops_current_environment_database
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("database" => "test-db")
+
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("database" => "secondary-test-db")
+
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
end
- def test_migrate_receives_correct_env_vars
- verbose, version = ENV["VERBOSE"], ENV["VERSION"]
+ def test_drops_current_environment_database_with_url
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("url" => "prod-db-url")
- ENV["VERBOSE"] = "false"
- ENV["VERSION"] = "4"
- ActiveRecord::Migrator.expects(:migrate).with("custom/path", 4)
- ActiveRecord::Migration.expects(:verbose=).with(false)
- ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose)
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("url" => "secondary-prod-db-url")
- ENV.delete("VERBOSE")
- ENV.delete("VERSION")
- ActiveRecord::Migrator.expects(:migrate).with("custom/path", nil)
- ActiveRecord::Migration.expects(:verbose=).with(true)
- ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose)
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
- ENV["VERBOSE"] = "yes"
- ENV["VERSION"] = "unknown"
- ActiveRecord::Migrator.expects(:migrate).with("custom/path", 0)
- ActiveRecord::Migration.expects(:verbose=).with(true)
- ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose)
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ def test_drops_test_and_development_databases_when_env_was_not_specified
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("database" => "dev-db")
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("database" => "secondary-dev-db")
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("database" => "test-db")
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("database" => "secondary-test-db")
+
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+
+ 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")
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("database" => "secondary-dev-db")
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("database" => "test-db")
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("database" => "secondary-test-db")
+
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
ensure
- ENV["VERBOSE"], ENV["VERSION"] = verbose, version
+ ENV["RAILS_ENV"] = old_env
+ end
+ end
+
+ if current_adapter?(:SQLite3Adapter) && !in_memory_db?
+ class DatabaseTasksMigrateTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ # Use a memory db here to avoid having to rollback at the end
+ setup do
+ migrations_path = MIGRATIONS_ROOT + "/valid"
+ file = ActiveRecord::Base.connection.raw_connection.filename
+ @conn = ActiveRecord::Base.establish_connection adapter: "sqlite3",
+ database: ":memory:", migrations_paths: migrations_path
+ source_db = SQLite3::Database.new file
+ dest_db = ActiveRecord::Base.connection.raw_connection
+ backup = SQLite3::Backup.new(dest_db, "main", source_db, "main")
+ backup.step(-1)
+ backup.finish
+ end
+
+ teardown do
+ @conn.release_connection if @conn
+ ActiveRecord::Base.establish_connection :arunit
+ end
+
+ def test_migrate_set_and_unset_verbose_and_version_env_vars
+ verbose, version = ENV["VERBOSE"], ENV["VERSION"]
+ ENV["VERSION"] = "2"
+ ENV["VERBOSE"] = "false"
+
+ # run down migration because it was already run on copied db
+ assert_empty capture_migration_output
+
+ ENV.delete("VERSION")
+ ENV.delete("VERBOSE")
+
+ # re-run up migration
+ assert_includes capture_migration_output, "migrating"
+ ensure
+ ENV["VERBOSE"], ENV["VERSION"] = verbose, version
+ end
+
+ def test_migrate_set_and_unset_empty_values_for_verbose_and_version_env_vars
+ verbose, version = ENV["VERBOSE"], ENV["VERSION"]
+
+ ENV["VERSION"] = "2"
+ ENV["VERBOSE"] = "false"
+
+ # run down migration because it was already run on copied db
+ assert_empty capture_migration_output
+
+ ENV["VERBOSE"] = ""
+ ENV["VERSION"] = ""
+
+ # re-run up migration
+ assert_includes capture_migration_output, "migrating"
+ ensure
+ ENV["VERBOSE"], ENV["VERSION"] = verbose, version
+ end
+
+ def test_migrate_set_and_unset_nonsense_values_for_verbose_and_version_env_vars
+ verbose, version = ENV["VERBOSE"], ENV["VERSION"]
+
+ # run down migration because it was already run on copied db
+ ENV["VERSION"] = "2"
+ ENV["VERBOSE"] = "false"
+
+ assert_empty capture_migration_output
+
+ ENV["VERBOSE"] = "yes"
+ ENV["VERSION"] = "2"
+
+ # run no migration because 2 was already run
+ assert_empty capture_migration_output
+ ensure
+ ENV["VERBOSE"], ENV["VERSION"] = verbose, version
+ end
+
+ private
+ def capture_migration_output
+ capture(:stdout) do
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
+ end
end
+ end
+
+ class DatabaseTasksMigrateErrorTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
- def test_migrate_raise_error_on_empty_version
+ def test_migrate_raise_error_on_invalid_version_format
version = ENV["VERSION"]
- ENV["VERSION"] = ""
+
+ ENV["VERSION"] = "unknown"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "0.1.11"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1.1.11"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "0 "
e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
- assert_equal "Empty VERSION provided", e.message
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1."
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1_"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1_name"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
ensure
ENV["VERSION"] = version
end
+ def test_migrate_raise_error_on_failed_check_target_version
+ ActiveRecord::Tasks::DatabaseTasks.stubs(:check_target_version).raises("foo")
+
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_equal "foo", e.message
+ end
+
def test_migrate_clears_schema_cache_afterward
- ActiveRecord::Base.expects(:clear_cache!)
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ assert_called(ActiveRecord::Base, :clear_cache!) do
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
end
end
@@ -404,9 +684,10 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
with("database" => "prod-db")
- ActiveRecord::Base.expects(:establish_connection).with(:production)
- ActiveRecord::Tasks::DatabaseTasks.purge_current("production")
+ assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do
+ ActiveRecord::Tasks::DatabaseTasks.purge_current("production")
+ end
end
end
@@ -444,6 +725,108 @@ module ActiveRecord
end
end
+ class DatabaseTaskTargetVersionTest < ActiveRecord::TestCase
+ def test_target_version_returns_nil_if_version_does_not_exist
+ version = ENV.delete("VERSION")
+ assert_nil ActiveRecord::Tasks::DatabaseTasks.target_version
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_target_version_returns_nil_if_version_is_empty
+ version = ENV["VERSION"]
+
+ ENV["VERSION"] = ""
+ assert_nil ActiveRecord::Tasks::DatabaseTasks.target_version
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_target_version_returns_converted_to_integer_env_version_if_version_exists
+ version = ENV["VERSION"]
+
+ ENV["VERSION"] = "0"
+ assert_equal ENV["VERSION"].to_i, ActiveRecord::Tasks::DatabaseTasks.target_version
+
+ ENV["VERSION"] = "42"
+ assert_equal ENV["VERSION"].to_i, ActiveRecord::Tasks::DatabaseTasks.target_version
+
+ ENV["VERSION"] = "042"
+ assert_equal ENV["VERSION"].to_i, ActiveRecord::Tasks::DatabaseTasks.target_version
+ ensure
+ ENV["VERSION"] = version
+ end
+ end
+
+ class DatabaseTaskCheckTargetVersionTest < ActiveRecord::TestCase
+ def test_check_target_version_does_not_raise_error_on_empty_version
+ version = ENV["VERSION"]
+ ENV["VERSION"] = ""
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_check_target_version_does_not_raise_error_if_version_is_not_setted
+ version = ENV.delete("VERSION")
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_check_target_version_raises_error_on_invalid_version_format
+ version = ENV["VERSION"]
+
+ ENV["VERSION"] = "unknown"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "0.1.11"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1.1.11"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "0 "
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1."
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1_"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1_name"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_check_target_version_does_not_raise_error_on_valid_version_format
+ version = ENV["VERSION"]
+
+ ENV["VERSION"] = "0"
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+
+ ENV["VERSION"] = "1"
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+
+ ENV["VERSION"] = "001"
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+
+ ENV["VERSION"] = "001_name.rb"
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ ensure
+ ENV["VERSION"] = version
+ end
+ end
+
class DatabaseTasksStructureDumpTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
@@ -468,8 +851,9 @@ module ActiveRecord
class DatabaseTasksCheckSchemaFileTest < ActiveRecord::TestCase
def test_check_schema_file
- Kernel.expects(:abort).with(regexp_matches(/awesome-file.sql/))
- ActiveRecord::Tasks::DatabaseTasks.check_schema_file("awesome-file.sql")
+ assert_called_with(Kernel, :abort, [/awesome-file.sql/]) do
+ ActiveRecord::Tasks::DatabaseTasks.check_schema_file("awesome-file.sql")
+ end
end
end
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index 98fe24baa0..6cddfaefeb 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -75,7 +75,7 @@ if current_adapter?(:Mysql2Adapter)
end
end
- class MysqlDBCreateAsRootTest < ActiveRecord::TestCase
+ class MysqlDBCreateWithInvalidPermissionsTest < ActiveRecord::TestCase
def setup
@connection = stub("Connection", create_database: true)
@error = Mysql2::Error.new("Invalid permissions")
@@ -86,13 +86,8 @@ if current_adapter?(:Mysql2Adapter)
"password" => "wossname"
}
- $stdin.stubs(:gets).returns("secret\n")
- $stdout.stubs(:print).returns(nil)
- @error.stubs(:errno).returns(1045)
ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).
- raises(@error).
- then.returns(true)
+ ActiveRecord::Base.stubs(:establish_connection).raises(@error)
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
@@ -102,75 +97,11 @@ if current_adapter?(:Mysql2Adapter)
$stdout, $stderr = @original_stdout, @original_stderr
end
- def test_root_password_is_requested
- assert_permissions_granted_for("pat")
- $stdin.expects(:gets).returns("secret\n")
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_connection_established_as_root
- assert_permissions_granted_for("pat")
- ActiveRecord::Base.expects(:establish_connection).with(
- "adapter" => "mysql2",
- "database" => nil,
- "username" => "root",
- "password" => "secret"
- )
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_database_created_by_root
- assert_permissions_granted_for("pat")
- @connection.expects(:create_database).
- with("my-app-db", {})
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_grant_privileges_for_normal_user
- assert_permissions_granted_for("pat")
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_do_not_grant_privileges_for_root_user
- @configuration["username"] = "root"
- @configuration["password"] = ""
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_connection_established_as_normal_user
- assert_permissions_granted_for("pat")
- ActiveRecord::Base.expects(:establish_connection).returns do
- ActiveRecord::Base.expects(:establish_connection).with(
- "adapter" => "mysql2",
- "database" => "my-app-db",
- "username" => "pat",
- "password" => "secret"
- )
-
- raise @error
+ def test_raises_error
+ assert_raises(Mysql2::Error) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
-
- def test_sends_output_to_stderr_when_other_errors
- @error.stubs(:errno).returns(42)
-
- $stderr.expects(:puts).at_least_once.returns(nil)
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- private
-
- def assert_permissions_granted_for(db_user)
- db_name = @configuration["database"]
- db_password = @configuration["password"]
- @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON `#{db_name}`.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;")
- end
end
class MySQLDBDropTest < ActiveRecord::TestCase
@@ -291,9 +222,14 @@ if current_adapter?(:Mysql2Adapter)
def test_structure_dump
filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ end
end
def test_structure_dump_with_extra_flags
@@ -311,39 +247,57 @@ if current_adapter?(:Mysql2Adapter)
filename = "awesome-file.sql"
ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"])
- Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--ignore-table=test-db.foo", "--ignore-table=test-db.bar", "test-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--ignore-table=test-db.foo", "--ignore-table=test-db.bar", "test-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ end
end
def test_warn_when_external_structure_dump_command_execution_fails
filename = "awesome-file.sql"
- Kernel.expects(:system)
- .with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db")
- .returns(false)
-
- e = assert_raise(RuntimeError) {
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- }
- assert_match(/^failed to execute: `mysqldump`$/, e.message)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"],
+ returns: false
+ ) do
+ e = assert_raise(RuntimeError) {
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ }
+ assert_match(/^failed to execute: `mysqldump`$/, e.message)
+ end
end
def test_structure_dump_with_port_number
filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(
- @configuration.merge("port" => 10000),
- filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(
+ @configuration.merge("port" => 10000),
+ filename)
+ end
end
def test_structure_dump_with_ssl
filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(
- @configuration.merge("sslca" => "ca.crt"),
- filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(
+ @configuration.merge("sslca" => "ca.crt"),
+ filename)
+ end
end
private
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index 6302e84884..a1a3700f07 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -229,7 +229,6 @@ if current_adapter?(:PostgreSQLAdapter)
ActiveRecord::Base.stubs(:connection).returns(@connection)
ActiveRecord::Base.stubs(:establish_connection).returns(true)
- Kernel.stubs(:system)
end
def teardown
@@ -237,9 +236,14 @@ if current_adapter?(:PostgreSQLAdapter)
end
def test_structure_dump
- Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
end
def test_structure_dump_header_comments_removed
@@ -262,47 +266,76 @@ if current_adapter?(:PostgreSQLAdapter)
end
def test_structure_dump_with_ignore_tables
- ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"])
-
- Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "-T", "foo", "-T", "bar", "my-app-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ assert_called(
+ ActiveRecord::SchemaDumper,
+ :ignore_tables,
+ returns: ["foo", "bar"]
+ ) do
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "-T", "foo", "-T", "bar", "my-app-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
+ end
end
def test_structure_dump_with_schema_search_path
@configuration["schema_search_path"] = "foo,bar"
- Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
end
def test_structure_dump_with_schema_search_path_and_dump_schemas_all
@configuration["schema_search_path"] = "foo,bar"
- Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db").returns(true)
-
- with_dump_schemas(:all) do
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db"],
+ returns: true
+ ) do
+ with_dump_schemas(:all) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
end
end
def test_structure_dump_with_dump_schemas_string
- Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db").returns(true)
-
- with_dump_schemas("foo,bar") do
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db"],
+ returns: true
+ ) do
+ with_dump_schemas("foo,bar") do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
end
end
def test_structure_dump_execution_fails
filename = "awesome-file.sql"
- Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", filename, "my-app-db").returns(nil)
-
- e = assert_raise(RuntimeError) do
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", filename, "my-app-db"],
+ returns: nil
+ ) do
+ e = assert_raise(RuntimeError) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ end
+ assert_match("failed to execute:", e.message)
end
- assert_match("failed to execute:", e.message)
end
private
@@ -333,14 +366,18 @@ if current_adapter?(:PostgreSQLAdapter)
ActiveRecord::Base.stubs(:connection).returns(@connection)
ActiveRecord::Base.stubs(:establish_connection).returns(true)
- Kernel.stubs(:system)
end
def test_structure_load
filename = "awesome-file.sql"
- Kernel.expects(:system).with("psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]).returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ end
end
def test_structure_load_with_extra_flags
@@ -356,9 +393,14 @@ if current_adapter?(:PostgreSQLAdapter)
def test_structure_load_accepts_path_with_spaces
filename = "awesome file.sql"
- Kernel.expects(:system).with("psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]).returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ end
end
private
diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb
index d7e3caa2ff..d368a7a6ee 100644
--- a/activerecord/test/cases/tasks/sqlite_rake_test.rb
+++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb
@@ -204,9 +204,9 @@ if current_adapter?(:SQLite3Adapter)
def test_structure_dump_with_ignore_tables
dbfile = @database
filename = "awesome-file.sql"
- ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo"])
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root")
+ assert_called(ActiveRecord::SchemaDumper, :ignore_tables, returns: ["foo"]) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root")
+ end
assert File.exist?(dbfile)
assert File.exist?(filename)
assert_match(/bar/, File.read(filename))
@@ -219,14 +219,19 @@ if current_adapter?(:SQLite3Adapter)
def test_structure_dump_execution_fails
dbfile = @database
filename = "awesome-file.sql"
- Kernel.expects(:system).with("sqlite3", "--noop", "db_create.sqlite3", ".schema", out: "awesome-file.sql").returns(nil)
-
- e = assert_raise(RuntimeError) do
- with_structure_dump_flags(["--noop"]) do
- quietly { ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root") }
+ assert_called_with(
+ Kernel,
+ :system,
+ ["sqlite3", "--noop", "db_create.sqlite3", ".schema", out: "awesome-file.sql"],
+ returns: nil
+ ) do
+ e = assert_raise(RuntimeError) do
+ with_structure_dump_flags(["--noop"]) do
+ quietly { ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root") }
+ end
end
+ assert_match("failed to execute:", e.message)
end
- assert_match("failed to execute:", e.message)
ensure
FileUtils.rm_f(filename)
FileUtils.rm_f(dbfile)
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index e57ebf56c8..024b5bd8a1 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -77,6 +77,10 @@ module ActiveRecord
model.reset_column_information
model.column_names.include?(column_name.to_s)
end
+
+ def frozen_error_class
+ Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError
+ end
end
class PostgreSQLTestCase < TestCase
@@ -110,9 +114,9 @@ module ActiveRecord
# FIXME: this needs to be refactored so specific database can add their own
# ignored SQL, or better yet, use a different notification for the queries
# instead examining the SQL content.
- oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im, /^\s*select .* from all_sequences/im]
mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im]
- postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i, /^\s*SELECT\b.*::regtype::oid\b/im]
sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im]
[oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb
index 41455637bb..086500de38 100644
--- a/activerecord/test/cases/time_precision_test.rb
+++ b/activerecord/test/cases/time_precision_test.rb
@@ -27,6 +27,24 @@ if subsecond_precision_supported?
assert_equal 6, Foo.columns_hash["finish"].precision
end
+ def test_time_precision_is_truncated_on_assignment
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :start, :time, precision: 0
+ @connection.add_column :foos, :finish, :time, precision: 6
+
+ time = ::Time.now.change(nsec: 123456789)
+ foo = Foo.new(start: time, finish: time)
+
+ assert_equal 0, foo.start.nsec
+ assert_equal 123456000, foo.finish.nsec
+
+ foo.save!
+ foo.reload
+
+ assert_equal 0, foo.start.nsec
+ assert_equal 123456000, foo.finish.nsec
+ end
+
def test_passing_precision_to_time_does_not_set_limit
@connection.create_table(:foos, force: true) do |t|
t.time :start, precision: 3
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index 5d3e2a175c..d9f7a81ce4 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -90,12 +90,22 @@ class TimestampTest < ActiveRecord::TestCase
@developer.touch(:created_at)
end
- assert !@developer.created_at_changed? , "created_at should not be changed"
+ assert !@developer.created_at_changed?, "created_at should not be changed"
assert !@developer.changed?, "record should not be changed"
assert_not_equal previously_created_at, @developer.created_at
assert_not_equal @previously_updated_at, @developer.updated_at
end
+ def test_touching_update_at_attribute_as_symbol_updates_timestamp
+ travel(1.second) do
+ @developer.touch(:updated_at)
+ end
+
+ assert_not @developer.updated_at_changed?
+ assert_not @developer.changed?
+ assert_not_equal @previously_updated_at, @developer.updated_at
+ end
+
def test_touching_an_attribute_updates_it
task = Task.first
previous_value = task.ending
@@ -139,13 +149,13 @@ class TimestampTest < ActiveRecord::TestCase
def test_touching_a_no_touching_object
Developer.no_touching do
- assert @developer.no_touching?
- assert !@owner.no_touching?
+ assert_predicate @developer, :no_touching?
+ assert_not_predicate @owner, :no_touching?
@developer.touch
end
- assert !@developer.no_touching?
- assert !@owner.no_touching?
+ assert_not_predicate @developer, :no_touching?
+ assert_not_predicate @owner, :no_touching?
assert_equal @previously_updated_at, @developer.updated_at
end
@@ -162,26 +172,26 @@ class TimestampTest < ActiveRecord::TestCase
def test_global_no_touching
ActiveRecord::Base.no_touching do
- assert @developer.no_touching?
- assert @owner.no_touching?
+ assert_predicate @developer, :no_touching?
+ assert_predicate @owner, :no_touching?
@developer.touch
end
- assert !@developer.no_touching?
- assert !@owner.no_touching?
+ assert_not_predicate @developer, :no_touching?
+ assert_not_predicate @owner, :no_touching?
assert_equal @previously_updated_at, @developer.updated_at
end
def test_no_touching_threadsafe
Thread.new do
Developer.no_touching do
- assert @developer.no_touching?
+ assert_predicate @developer, :no_touching?
sleep(1)
end
end
- assert !@developer.no_touching?
+ assert_not_predicate @developer, :no_touching?
end
def test_no_touching_with_callbacks
@@ -237,7 +247,7 @@ class TimestampTest < ActiveRecord::TestCase
pet = Pet.new(owner: klass.new)
pet.save!
- assert pet.owner.new_record?
+ assert_predicate pet.owner, :new_record?
end
def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute
diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb
index 1757031371..925a4609a2 100644
--- a/activerecord/test/cases/touch_later_test.rb
+++ b/activerecord/test/cases/touch_later_test.rb
@@ -13,7 +13,7 @@ class TouchLaterTest < ActiveRecord::TestCase
def test_touch_laster_raise_if_non_persisted
invoice = Invoice.new
Invoice.transaction do
- assert_not invoice.persisted?
+ assert_not_predicate invoice, :persisted?
assert_raises(ActiveRecord::ActiveRecordError) do
invoice.touch_later
end
@@ -23,7 +23,7 @@ class TouchLaterTest < ActiveRecord::TestCase
def test_touch_later_dont_set_dirty_attributes
invoice = Invoice.create!
invoice.touch_later
- assert_not invoice.changed?
+ assert_not_predicate invoice, :changed?
end
def test_touch_later_respects_no_touching_policy
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index 1f370a80ee..e89ac53732 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -158,13 +158,13 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
def test_only_call_after_commit_on_top_level_transactions
@first.after_commit_block { |r| r.history << :after_commit }
- assert @first.history.empty?
+ assert_empty @first.history
@first.transaction do
@first.transaction(requires_new: true) do
@first.touch
end
- assert @first.history.empty?
+ assert_empty @first.history
end
assert_equal [:after_commit], @first.history
end
@@ -394,6 +394,28 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
end
+class TransactionAfterCommitCallbacksWithOptimisticLockingTest < ActiveRecord::TestCase
+ class PersonWithCallbacks < ActiveRecord::Base
+ self.table_name = :people
+
+ after_create_commit { |record| record.history << :commit_on_create }
+ after_update_commit { |record| record.history << :commit_on_update }
+ after_destroy_commit { |record| record.history << :commit_on_destroy }
+
+ def history
+ @history ||= []
+ end
+ end
+
+ def test_after_commit_callbacks_with_optimistic_locking
+ person = PersonWithCallbacks.create!(first_name: "first name")
+ person.update!(first_name: "another name")
+ person.destroy
+
+ assert_equal [:commit_on_create, :commit_on_update, :commit_on_destroy], person.history
+ end
+end
+
class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
self.use_transactional_tests = false
@@ -518,7 +540,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
@topic.content = "foo"
@topic.save!
end
- assert @topic.history.empty?
+ assert_empty @topic.history
end
def test_commit_run_transactions_callbacks_with_explicit_enrollment
@@ -538,7 +560,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
@topic.save!
raise ActiveRecord::Rollback
end
- assert @topic.history.empty?
+ assert_empty @topic.history
end
def test_rollback_run_transactions_callbacks_with_explicit_enrollment
diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb
index b1ebccdcc3..eaafd13360 100644
--- a/activerecord/test/cases/transaction_isolation_test.rb
+++ b/activerecord/test/cases/transaction_isolation_test.rb
@@ -15,9 +15,7 @@ unless ActiveRecord::Base.connection.supports_transaction_isolation?
end
end
end
-end
-
-if ActiveRecord::Base.connection.supports_transaction_isolation?
+else
class TransactionIsolationTest < ActiveRecord::TestCase
self.use_transactional_tests = false
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 7fd125ab74..3fd38b4b60 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -20,22 +20,22 @@ class TransactionTest < ActiveRecord::TestCase
def test_persisted_in_a_model_with_custom_primary_key_after_failed_save
movie = Movie.create
- assert !movie.persisted?
+ assert_not_predicate movie, :persisted?
end
def test_raise_after_destroy
- assert_not @first.frozen?
+ assert_not_predicate @first, :frozen?
assert_raises(RuntimeError) {
Topic.transaction do
@first.destroy
- assert @first.frozen?
+ assert_predicate @first, :frozen?
raise
end
}
assert @first.reload
- assert_not @first.frozen?
+ assert_not_predicate @first, :frozen?
end
def test_successful
@@ -152,14 +152,14 @@ class TransactionTest < ActiveRecord::TestCase
@first.approved = true
e = assert_raises(RuntimeError) { @first.save }
assert_equal "Make the transaction rollback", e.message
- assert !Topic.find(1).approved?
+ assert_not_predicate Topic.find(1), :approved?
end
def test_rolling_back_in_a_callback_rollbacks_before_save
def @first.before_save_for_transaction
raise ActiveRecord::Rollback
end
- assert !@first.approved
+ assert_not @first.approved
Topic.transaction do
@first.approved = true
@@ -186,7 +186,7 @@ class TransactionTest < ActiveRecord::TestCase
author = Author.create! name: "foo"
author.name = nil
assert_not author.save
- assert_not author.new_record?
+ assert_not_predicate author, :new_record?
end
def test_update_should_rollback_on_failure
@@ -194,7 +194,7 @@ class TransactionTest < ActiveRecord::TestCase
posts_count = author.posts.size
assert posts_count > 0
status = author.update(name: nil, post_ids: [])
- assert !status
+ assert_not status
assert_equal posts_count, author.posts.reload.size
end
@@ -212,7 +212,7 @@ class TransactionTest < ActiveRecord::TestCase
add_cancelling_before_destroy_with_db_side_effect_to_topic @first
nbooks_before_destroy = Book.count
status = @first.destroy
- assert !status
+ assert_not status
@first.reload
assert_equal nbooks_before_destroy, Book.count
end
@@ -224,7 +224,7 @@ class TransactionTest < ActiveRecord::TestCase
original_author_name = @first.author_name
@first.author_name += "_this_should_not_end_up_in_the_db"
status = @first.save
- assert !status
+ assert_not status
assert_equal original_author_name, @first.reload.author_name
assert_equal nbooks_before_save, Book.count
end
@@ -323,8 +323,8 @@ class TransactionTest < ActiveRecord::TestCase
raise ActiveRecord::Rollback
end
- refute_predicate topic_one, :persisted?
- refute_predicate topic_two, :persisted?
+ assert_not_predicate topic_one, :persisted?
+ assert_not_predicate topic_two, :persisted?
end
def test_nested_transaction_without_new_transaction_applies_parent_state_on_rollback
@@ -344,8 +344,8 @@ class TransactionTest < ActiveRecord::TestCase
raise ActiveRecord::Rollback
end
- refute_predicate topic_one, :persisted?
- refute_predicate topic_two, :persisted?
+ assert_not_predicate topic_one, :persisted?
+ assert_not_predicate topic_two, :persisted?
end
def test_double_nested_transaction_applies_parent_state_on_rollback
@@ -371,9 +371,9 @@ class TransactionTest < ActiveRecord::TestCase
raise ActiveRecord::Rollback
end
- refute_predicate topic_one, :persisted?
- refute_predicate topic_two, :persisted?
- refute_predicate topic_three, :persisted?
+ assert_not_predicate topic_one, :persisted?
+ assert_not_predicate topic_two, :persisted?
+ assert_not_predicate topic_three, :persisted?
end
def test_manually_rolling_back_a_transaction
@@ -417,8 +417,8 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- assert @first.reload.approved?
- assert !@second.reload.approved?
+ assert_predicate @first.reload, :approved?
+ assert_not_predicate @second.reload, :approved?
end if Topic.connection.supports_savepoints?
def test_force_savepoint_on_instance
@@ -438,8 +438,8 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- assert @first.reload.approved?
- assert !@second.reload.approved?
+ assert_predicate @first.reload, :approved?
+ assert_not_predicate @second.reload, :approved?
end if Topic.connection.supports_savepoints?
def test_no_savepoint_in_nested_transaction_without_force
@@ -459,8 +459,8 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- assert !@first.reload.approved?
- assert !@second.reload.approved?
+ assert_not_predicate @first.reload, :approved?
+ assert_not_predicate @second.reload, :approved?
end if Topic.connection.supports_savepoints?
def test_many_savepoints
@@ -516,12 +516,12 @@ class TransactionTest < ActiveRecord::TestCase
@first.approved = false
@first.save!
Topic.connection.rollback_to_savepoint("first")
- assert @first.reload.approved?
+ assert_predicate @first.reload, :approved?
@first.approved = false
@first.save!
Topic.connection.release_savepoint("first")
- assert_not @first.reload.approved?
+ assert_not_predicate @first.reload, :approved?
end
end if Topic.connection.supports_savepoints?
@@ -576,7 +576,7 @@ class TransactionTest < ActiveRecord::TestCase
def test_rollback_when_saving_a_frozen_record
topic = Topic.new(title: "test")
topic.freeze
- e = assert_raise(RuntimeError) { topic.save }
+ e = assert_raise(frozen_error_class) { topic.save }
# Not good enough, but we can't do much
# about it since there is no specific error
# for frozen objects.
@@ -663,8 +663,8 @@ class TransactionTest < ActiveRecord::TestCase
raise ActiveRecord::Rollback
end
- assert_not reply.frozen?
- assert_not topic.frozen?
+ assert_not_predicate reply, :frozen?
+ assert_not_predicate topic, :frozen?
end
def test_restore_id_after_rollback
@@ -819,28 +819,28 @@ class TransactionTest < ActiveRecord::TestCase
connection = Topic.connection
transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
- assert transaction.open?
- assert !transaction.state.rolledback?
- assert !transaction.state.committed?
+ assert_predicate transaction, :open?
+ assert_not_predicate transaction.state, :rolledback?
+ assert_not_predicate transaction.state, :committed?
transaction.rollback
- assert transaction.state.rolledback?
- assert !transaction.state.committed?
+ assert_predicate transaction.state, :rolledback?
+ assert_not_predicate transaction.state, :committed?
end
def test_transactions_state_from_commit
connection = Topic.connection
transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
- assert transaction.open?
- assert !transaction.state.rolledback?
- assert !transaction.state.committed?
+ assert_predicate transaction, :open?
+ assert_not_predicate transaction.state, :rolledback?
+ assert_not_predicate transaction.state, :committed?
transaction.commit
- assert !transaction.state.rolledback?
- assert transaction.state.committed?
+ assert_not_predicate transaction.state, :rolledback?
+ assert_predicate transaction.state, :committed?
end
def test_set_state_method_is_deprecated
@@ -929,7 +929,7 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
raise
end
rescue
- assert !@first.reload.approved?
+ assert_not_predicate @first.reload, :approved?
end
end
@@ -950,31 +950,29 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
end
end
- assert !@first.reload.approved?
+ assert_not_predicate @first.reload, :approved?
end
end if Topic.connection.supports_savepoints?
-if current_adapter?(:PostgreSQLAdapter)
+if ActiveRecord::Base.connection.supports_transaction_isolation?
class ConcurrentTransactionTest < TransactionTest
# This will cause transactions to overlap and fail unless they are performed on
# separate database connections.
- unless in_memory_db?
- def test_transaction_per_thread
- threads = 3.times.map do
- Thread.new do
- Topic.transaction do
- topic = Topic.find(1)
- topic.approved = !topic.approved?
- assert topic.save!
- topic.approved = !topic.approved?
- assert topic.save!
- end
- Topic.connection.close
+ def test_transaction_per_thread
+ threads = 3.times.map do
+ Thread.new do
+ Topic.transaction do
+ topic = Topic.find(1)
+ topic.approved = !topic.approved?
+ assert topic.save!
+ topic.approved = !topic.approved?
+ assert topic.save!
end
+ Topic.connection.close
end
-
- threads.each(&:join)
end
+
+ threads.each(&:join)
end
# Test for dirty reads among simultaneous transactions.
diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb
index 8c51b30fdd..9e7810a6a5 100644
--- a/activerecord/test/cases/type/string_test.rb
+++ b/activerecord/test/cases/type/string_test.rb
@@ -9,16 +9,16 @@ module ActiveRecord
klass.table_name = "authors"
author = klass.create!(name: "Sean")
- assert_not author.changed?
+ assert_not_predicate author, :changed?
author.name << " Griffin"
- assert author.name_changed?
+ assert_predicate author, :name_changed?
author.save!
author.reload
assert_equal "Sean Griffin", author.name
- assert_not author.changed?
+ assert_not_predicate author, :changed?
end
end
end
diff --git a/activerecord/test/cases/unsafe_raw_sql_test.rb b/activerecord/test/cases/unsafe_raw_sql_test.rb
new file mode 100644
index 0000000000..d5d8f2a09a
--- /dev/null
+++ b/activerecord/test/cases/unsafe_raw_sql_test.rb
@@ -0,0 +1,319 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/comment"
+
+class UnsafeRawSqlTest < ActiveRecord::TestCase
+ fixtures :posts, :comments
+
+ test "order: allows string column name" do
+ ids_expected = Post.order(Arel.sql("title")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("title").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("title").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows symbol column name" do
+ ids_expected = Post.order(Arel.sql("title")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:title).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:title).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows downcase symbol direction" do
+ ids_expected = Post.order(Arel.sql("title") => Arel.sql("asc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: :asc).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: :asc).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows upcase symbol direction" do
+ ids_expected = Post.order(Arel.sql("title") => Arel.sql("ASC")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: :ASC).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: :ASC).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows string direction" do
+ ids_expected = Post.order(Arel.sql("title") => Arel.sql("asc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: "asc").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: "asc").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows multiple columns" do
+ ids_expected = Post.order(Arel.sql("author_id"), Arel.sql("title")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:author_id, :title).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:author_id, :title).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows mixed" do
+ ids_expected = Post.order(Arel.sql("author_id"), Arel.sql("title") => Arel.sql("asc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:author_id, title: :asc).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:author_id, title: :asc).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows table and column name" do
+ ids_expected = Post.order(Arel.sql("title")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("posts.title").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("posts.title").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows column name and direction in string" do
+ ids_expected = Post.order(Arel.sql("title desc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("title desc").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("title desc").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows table name, column name and direction in string" do
+ ids_expected = Post.order(Arel.sql("title desc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("posts.title desc").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("posts.title desc").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows NULLS FIRST and NULLS LAST too" do
+ raise "precondition failed" if Post.count < 2
+
+ # Ensure there are NULL and non-NULL post types.
+ Post.first.update_column(:type, nil)
+ Post.last.update_column(:type, "Programming")
+
+ ["asc", "desc", ""].each do |direction|
+ %w(first last).each do |position|
+ ids_expected = Post.order(Arel.sql("type #{direction} nulls #{position}")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("type #{direction} nulls #{position}").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("type #{direction} nulls #{position}").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+ end
+ end if current_adapter?(:PostgreSQLAdapter)
+
+ test "order: disallows invalid column name" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.order("len(title) asc").pluck(:id)
+ end
+ end
+ end
+
+ test "order: disallows invalid direction" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ArgumentError) do
+ Post.order(title: :foo).pluck(:id)
+ end
+ end
+ end
+
+ test "order: disallows invalid column with direction" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.order("len(title)" => :asc).pluck(:id)
+ end
+ end
+ end
+
+ test "order: always allows Arel" do
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(Arel.sql("length(title)")).pluck(:title) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(Arel.sql("length(title)")).pluck(:title) }
+
+ assert_equal ids_depr, ids_disabled
+ end
+
+ test "order: allows Arel.sql with binds" do
+ ids_expected = Post.order(Arel.sql("REPLACE(title, 'misc', 'zzzz'), id")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order([Arel.sql("REPLACE(title, ?, ?), id"), "misc", "zzzz"]).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order([Arel.sql("REPLACE(title, ?, ?), id"), "misc", "zzzz"]).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: disallows invalid bind statement" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.order(["REPLACE(title, ?, ?), id", "misc", "zzzz"]).pluck(:id)
+ end
+ end
+ end
+
+ test "order: disallows invalid Array arguments" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.order(["author_id", "length(title)"]).pluck(:id)
+ end
+ end
+ end
+
+ test "order: allows valid Array arguments" do
+ ids_expected = Post.order(Arel.sql("author_id, length(title)")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(["author_id", Arel.sql("length(title)")]).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(["author_id", Arel.sql("length(title)")]).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: logs deprecation warning for unrecognized column" do
+ with_unsafe_raw_sql_deprecated do
+ assert_deprecated(/Dangerous query method/) do
+ Post.order("length(title)")
+ end
+ end
+ end
+
+ test "pluck: allows string column name" do
+ titles_expected = Post.pluck(Arel.sql("title"))
+
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("title") }
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("title") }
+
+ assert_equal titles_expected, titles_depr
+ assert_equal titles_expected, titles_disabled
+ end
+
+ test "pluck: allows symbol column name" do
+ titles_expected = Post.pluck(Arel.sql("title"))
+
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:title) }
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:title) }
+
+ assert_equal titles_expected, titles_depr
+ assert_equal titles_expected, titles_disabled
+ end
+
+ test "pluck: allows multiple column names" do
+ values_expected = Post.pluck(Arel.sql("title"), Arel.sql("id"))
+
+ values_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:title, :id) }
+ values_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:title, :id) }
+
+ assert_equal values_expected, values_depr
+ assert_equal values_expected, values_disabled
+ end
+
+ test "pluck: allows column names with includes" do
+ values_expected = Post.includes(:comments).pluck(Arel.sql("title"), Arel.sql("id"))
+
+ values_depr = with_unsafe_raw_sql_deprecated { Post.includes(:comments).pluck(:title, :id) }
+ values_disabled = with_unsafe_raw_sql_disabled { Post.includes(:comments).pluck(:title, :id) }
+
+ assert_equal values_expected, values_depr
+ assert_equal values_expected, values_disabled
+ end
+
+ test "pluck: allows auto-generated attributes" do
+ values_expected = Post.pluck(Arel.sql("tags_count"))
+
+ values_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:tags_count) }
+ values_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:tags_count) }
+
+ assert_equal values_expected, values_depr
+ assert_equal values_expected, values_disabled
+ end
+
+ test "pluck: allows table and column names" do
+ titles_expected = Post.pluck(Arel.sql("title"))
+
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("posts.title") }
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("posts.title") }
+
+ assert_equal titles_expected, titles_depr
+ assert_equal titles_expected, titles_disabled
+ end
+
+ test "pluck: disallows invalid column name" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.pluck("length(title)")
+ end
+ end
+ end
+
+ test "pluck: disallows invalid column name amongst valid names" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.pluck(:title, "length(title)")
+ end
+ end
+ end
+
+ test "pluck: disallows invalid column names with includes" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.includes(:comments).pluck(:title, "length(title)")
+ end
+ end
+ end
+
+ test "pluck: always allows Arel" do
+ values_depr = with_unsafe_raw_sql_deprecated { Post.includes(:comments).pluck(:title, Arel.sql("length(title)")) }
+ values_disabled = with_unsafe_raw_sql_disabled { Post.includes(:comments).pluck(:title, Arel.sql("length(title)")) }
+
+ assert_equal values_depr, values_disabled
+ end
+
+ test "pluck: logs deprecation warning" do
+ with_unsafe_raw_sql_deprecated do
+ assert_deprecated(/Dangerous query method/) do
+ Post.includes(:comments).pluck(:title, "length(title)")
+ end
+ end
+ end
+
+ def with_unsafe_raw_sql_disabled(&blk)
+ with_config(:disabled, &blk)
+ end
+
+ def with_unsafe_raw_sql_deprecated(&blk)
+ with_config(:deprecated, &blk)
+ end
+
+ def with_config(new_value, &blk)
+ old_value = ActiveRecord::Base.allow_unsafe_raw_sql
+ ActiveRecord::Base.allow_unsafe_raw_sql = new_value
+ blk.call
+ ensure
+ ActiveRecord::Base.allow_unsafe_raw_sql = old_value
+ end
+end
diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb
index a997f8be9c..8235a54d8a 100644
--- a/activerecord/test/cases/validations/absence_validation_test.rb
+++ b/activerecord/test/cases/validations/absence_validation_test.rb
@@ -13,8 +13,8 @@ class AbsenceValidationTest < ActiveRecord::TestCase
validates_absence_of :name
end
- assert boy_klass.new.valid?
- assert_not boy_klass.new(name: "Alex").valid?
+ assert_predicate boy_klass.new, :valid?
+ assert_not_predicate boy_klass.new(name: "Alex"), :valid?
end
def test_has_one_marked_for_destruction
@@ -44,7 +44,7 @@ class AbsenceValidationTest < ActiveRecord::TestCase
assert_not boy.valid?, "should not be valid if has_many association is present"
i2.mark_for_destruction
- assert boy.valid?
+ assert_predicate boy, :valid?
end
def test_does_not_call_to_a_on_associations
@@ -65,11 +65,11 @@ class AbsenceValidationTest < ActiveRecord::TestCase
Interest.validates_absence_of(:token)
interest = Interest.create!(topic: "Thought Leadering")
- assert interest.valid?
+ assert_predicate interest, :valid?
interest.token = "tl"
- assert interest.invalid?
+ assert_predicate interest, :invalid?
end
end
end
diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb
index 80fe375ae5..ce6d42b34b 100644
--- a/activerecord/test/cases/validations/association_validation_test.rb
+++ b/activerecord/test/cases/validations/association_validation_test.rb
@@ -16,14 +16,14 @@ class AssociationValidationTest < ActiveRecord::TestCase
Reply.validates_presence_of(:content)
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
t.replies << [r = Reply.new("title" => "A reply"), r2 = Reply.new("title" => "Another reply", "content" => "non-empty"), r3 = Reply.new("title" => "Yet another reply"), r4 = Reply.new("title" => "The last reply", "content" => "non-empty")]
- assert !t.valid?
- assert t.errors[:replies].any?
+ assert_not_predicate t, :valid?
+ assert_predicate t.errors[:replies], :any?
assert_equal 1, r.errors.count # make sure all associated objects have been validated
assert_equal 0, r2.errors.count
assert_equal 1, r3.errors.count
assert_equal 0, r4.errors.count
r.content = r3.content = "non-empty"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_associated_one
@@ -31,10 +31,10 @@ class AssociationValidationTest < ActiveRecord::TestCase
Topic.validates_presence_of(:content)
r = Reply.new("title" => "A reply", "content" => "with content!")
r.topic = Topic.create("title" => "uhohuhoh")
- assert !r.valid?
- assert r.errors[:topic].any?
+ assert_not_predicate r, :valid?
+ assert_predicate r.errors[:topic], :any?
r.topic.content = "non-empty"
- assert r.valid?
+ assert_predicate r, :valid?
end
def test_validates_associated_marked_for_destruction
@@ -42,9 +42,9 @@ class AssociationValidationTest < ActiveRecord::TestCase
Reply.validates_presence_of(:content)
t = Topic.new
t.replies << Reply.new
- assert t.invalid?
+ assert_predicate t, :invalid?
t.replies.first.mark_for_destruction
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_associated_without_marked_for_destruction
@@ -56,7 +56,7 @@ class AssociationValidationTest < ActiveRecord::TestCase
Topic.validates_associated(:replies)
t = Topic.new
t.define_singleton_method(:replies) { [reply.new] }
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_associated_with_custom_message_using_quotes
@@ -71,11 +71,11 @@ class AssociationValidationTest < ActiveRecord::TestCase
def test_validates_associated_missing
Reply.validates_presence_of(:topic)
r = Reply.create("title" => "A reply", "content" => "with content!")
- assert !r.valid?
- assert r.errors[:topic].any?
+ assert_not_predicate r, :valid?
+ assert_predicate r.errors[:topic], :any?
r.topic = Topic.first
- assert r.valid?
+ assert_predicate r, :valid?
end
def test_validates_presence_of_belongs_to_association__parent_is_new_record
diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb
index 87ce4c6f37..1fbcdc271b 100644
--- a/activerecord/test/cases/validations/length_validation_test.rb
+++ b/activerecord/test/cases/validations/length_validation_test.rb
@@ -17,48 +17,48 @@ class LengthValidationTest < ActiveRecord::TestCase
def test_validates_size_of_association
assert_nothing_raised { @owner.validates_size_of :pets, minimum: 1 }
o = @owner.new("name" => "nopets")
- assert !o.save
- assert o.errors[:pets].any?
+ assert_not o.save
+ assert_predicate o.errors[:pets], :any?
o.pets.build("name" => "apet")
- assert o.valid?
+ assert_predicate o, :valid?
end
def test_validates_size_of_association_using_within
assert_nothing_raised { @owner.validates_size_of :pets, within: 1..2 }
o = @owner.new("name" => "nopets")
- assert !o.save
- assert o.errors[:pets].any?
+ assert_not o.save
+ assert_predicate o.errors[:pets], :any?
o.pets.build("name" => "apet")
- assert o.valid?
+ assert_predicate o, :valid?
2.times { o.pets.build("name" => "apet") }
- assert !o.save
- assert o.errors[:pets].any?
+ assert_not o.save
+ assert_predicate o.errors[:pets], :any?
end
def test_validates_size_of_association_utf8
@owner.validates_size_of :pets, minimum: 1
o = @owner.new("name" => "あいうえおかきくけこ")
- assert !o.save
- assert o.errors[:pets].any?
+ assert_not o.save
+ assert_predicate o.errors[:pets], :any?
o.pets.build("name" => "あいうえおかきくけこ")
- assert o.valid?
+ assert_predicate o, :valid?
end
def test_validates_size_of_respects_records_marked_for_destruction
@owner.validates_size_of :pets, minimum: 1
owner = @owner.new
assert_not owner.save
- assert owner.errors[:pets].any?
+ assert_predicate owner.errors[:pets], :any?
pet = owner.pets.build
- assert owner.valid?
+ assert_predicate owner, :valid?
assert owner.save
pet_count = Pet.count
- assert_not owner.update_attributes pets_attributes: [ { _destroy: 1, id: pet.id } ]
- assert_not owner.valid?
- assert owner.errors[:pets].any?
+ assert_not owner.update pets_attributes: [ { _destroy: 1, id: pet.id } ]
+ assert_not_predicate owner, :valid?
+ assert_predicate owner.errors[:pets], :any?
assert_equal pet_count, Pet.count
end
@@ -70,11 +70,11 @@ class LengthValidationTest < ActiveRecord::TestCase
pet = Pet.create!(name: "Fancy Pants", nickname: "Fancy")
- assert pet.valid?
+ assert_predicate pet, :valid?
pet.nickname = ""
- assert pet.invalid?
+ assert_predicate 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 3ab1567b51..63c3f67da2 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -15,10 +15,10 @@ class PresenceValidationTest < ActiveRecord::TestCase
def test_validates_presence_of_non_association
Boy.validates_presence_of(:name)
b = Boy.new
- assert b.invalid?
+ assert_predicate b, :invalid?
b.name = "Alex"
- assert b.valid?
+ assert_predicate b, :valid?
end
def test_validates_presence_of_has_one
@@ -33,23 +33,23 @@ class PresenceValidationTest < ActiveRecord::TestCase
b = Boy.new
f = Face.new
b.face = f
- assert b.valid?
+ assert_predicate b, :valid?
f.mark_for_destruction
- assert b.invalid?
+ assert_predicate b, :invalid?
end
def test_validates_presence_of_has_many_marked_for_destruction
Boy.validates_presence_of(:interests)
b = Boy.new
b.interests << [i1 = Interest.new, i2 = Interest.new]
- assert b.valid?
+ assert_predicate b, :valid?
i1.mark_for_destruction
- assert b.valid?
+ assert_predicate b, :valid?
i2.mark_for_destruction
- assert b.invalid?
+ assert_predicate b, :invalid?
end
def test_validates_presence_doesnt_convert_to_array
@@ -74,11 +74,11 @@ class PresenceValidationTest < ActiveRecord::TestCase
Interest.validates_presence_of(:abbreviation)
interest = Interest.create!(topic: "Thought Leadering", abbreviation: "tl")
- assert interest.valid?
+ assert_predicate interest, :valid?
interest.abbreviation = ""
- assert interest.invalid?
+ assert_predicate interest, :invalid?
end
end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index a10567f066..941aed5402 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -62,7 +62,7 @@ class TopicWithAfterCreate < Topic
after_create :set_author
def set_author
- update_attributes!(author_name: "#{title} #{id}")
+ update!(author_name: "#{title} #{id}")
end
end
@@ -96,7 +96,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
Topic.validates_uniqueness_of(:new_title)
topic = Topic.new(new_title: "abc")
- assert topic.valid?
+ assert_predicate topic, :valid?
end
def test_validates_uniqueness_with_nil_value
@@ -116,7 +116,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
Topic.create!("title" => "abc")
t2 = Topic.new("title" => "abc")
- assert !t2.valid?
+ assert_not_predicate t2, :valid?
assert t2.errors[:title]
end
@@ -259,15 +259,15 @@ class UniquenessValidationTest < ActiveRecord::TestCase
t2 = Topic.new("title" => "I'm UNIQUE!", :parent_id => 1)
assert !t2.valid?, "Shouldn't be valid"
assert !t2.save, "Shouldn't save t2 as unique"
- assert t2.errors[:title].any?
- assert t2.errors[:parent_id].any?
+ assert_predicate t2.errors[:title], :any?
+ assert_predicate t2.errors[:parent_id], :any?
assert_equal ["has already been taken"], t2.errors[:title]
t2.title = "I'm truly UNIQUE!"
assert !t2.valid?, "Shouldn't be valid"
assert !t2.save, "Shouldn't save t2 as unique"
- assert t2.errors[:title].empty?
- assert t2.errors[:parent_id].any?
+ assert_empty t2.errors[:title]
+ assert_predicate t2.errors[:parent_id], :any?
t2.parent_id = 4
assert t2.save, "Should now save t2 as unique"
@@ -326,15 +326,15 @@ class UniquenessValidationTest < ActiveRecord::TestCase
t2 = Topic.new("title" => "I'M UNIQUE!")
assert t2.valid?, "Should be valid"
assert t2.save, "Should save t2 as unique"
- assert t2.errors[:title].empty?
- assert t2.errors[:parent_id].empty?
+ assert_empty t2.errors[:title]
+ assert_empty t2.errors[:parent_id]
assert_not_equal ["has already been taken"], t2.errors[:title]
t3 = Topic.new("title" => "I'M uNiQUe!")
assert t3.valid?, "Should be valid"
assert t3.save, "Should save t2 as unique"
- assert t3.errors[:title].empty?
- assert t3.errors[:parent_id].empty?
+ assert_empty t3.errors[:title]
+ assert_empty t3.errors[:parent_id]
assert_not_equal ["has already been taken"], t3.errors[:title]
end
@@ -343,7 +343,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
Topic.create!("title" => 101)
t2 = Topic.new("title" => 101)
- assert !t2.valid?
+ assert_not_predicate t2, :valid?
assert t2.errors[:title]
end
@@ -360,7 +360,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
t1 = Topic.new("title" => "I'm unique!", "author_name" => "Mary")
assert t1.save
t2 = Topic.new("title" => "I'm unique!", "author_name" => "David")
- assert !t2.valid?
+ assert_not_predicate t2, :valid?
end
end
@@ -460,16 +460,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase
def test_validate_uniqueness_on_existing_relation
event = Event.create
- assert TopicWithUniqEvent.create(event: event).valid?
+ assert_predicate TopicWithUniqEvent.create(event: event), :valid?
topic = TopicWithUniqEvent.new(event: event)
- assert_not topic.valid?
+ assert_not_predicate topic, :valid?
assert_equal ["has already been taken"], topic.errors[:event]
end
def test_validate_uniqueness_on_empty_relation
topic = TopicWithUniqEvent.new
- assert topic.valid?
+ assert_predicate topic, :valid?
end
def test_validate_uniqueness_of_custom_primary_key
@@ -488,7 +488,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
key2 = klass.create!(key_number: 11)
key2.key_number = 10
- assert_not key2.valid?
+ assert_not_predicate key2, :valid?
end
def test_validate_uniqueness_without_primary_key
@@ -501,8 +501,8 @@ class UniquenessValidationTest < ActiveRecord::TestCase
end
abc = klass.create!(dashboard_id: "abc")
- assert klass.new(dashboard_id: "xyz").valid?
- assert_not klass.new(dashboard_id: "abc").valid?
+ assert_predicate klass.new(dashboard_id: "xyz"), :valid?
+ assert_not_predicate klass.new(dashboard_id: "abc"), :valid?
abc.dashboard_id = "def"
@@ -530,7 +530,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert topic.author_name.start_with?("Title1")
topic2 = TopicWithAfterCreate.new(title: "Title1")
- refute topic2.valid?
+ assert_not_predicate topic2, :valid?
assert_equal(["has already been taken"], topic2.errors[:title])
end
@@ -550,7 +550,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert_empty item.errors
item2 = CoolTopic.new(id: item.id, title: "MyItem2")
- refute item2.valid?
+ assert_not_predicate item2, :valid?
assert_equal(["has already been taken"], item2.errors[:id])
end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 7f84939027..a33877f43a 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -19,7 +19,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_valid_uses_create_context_when_new
r = WrongReply.new
r.title = "Wrong Create"
- assert_not r.valid?
+ assert_not_predicate r, :valid?
assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid"
assert_equal ["is Wrong Create"], r.errors[:title], "A reply with a bad content should contain an error"
end
@@ -39,7 +39,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_valid_using_special_context
r = WrongReply.new(title: "Valid title")
- assert !r.valid?(:special_case)
+ assert_not r.valid?(:special_case)
assert_equal "Invalid", r.errors[:author_name].join
r.author_name = "secret"
@@ -125,7 +125,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_save_without_validation
reply = WrongReply.new
- assert !reply.save
+ assert_not reply.save
assert reply.save(validate: false)
end
@@ -139,7 +139,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_throw_away_typing
d = Developer.new("name" => "David", "salary" => "100,000")
- assert !d.valid?
+ assert_not_predicate d, :valid?
assert_equal 100, d.salary
assert_equal "100,000", d.salary_before_type_cast
end
@@ -166,7 +166,7 @@ class ValidationsTest < ActiveRecord::TestCase
topic = klass.new(wibble: "123-4567")
topic.wibble.gsub!("-", "")
- assert topic.valid?
+ assert_predicate topic, :valid?
end
def test_numericality_validation_checks_against_raw_value
@@ -175,12 +175,12 @@ class ValidationsTest < ActiveRecord::TestCase
ActiveModel::Name.new(self, nil, "Topic")
end
attribute :wibble, :decimal, scale: 2, precision: 9
- validates_numericality_of :wibble, greater_than_or_equal_to: BigDecimal.new("97.18")
+ validates_numericality_of :wibble, greater_than_or_equal_to: BigDecimal("97.18")
end
- assert_not klass.new(wibble: "97.179").valid?
- assert_not klass.new(wibble: 97.179).valid?
- assert_not klass.new(wibble: BigDecimal.new("97.179")).valid?
+ assert_not_predicate klass.new(wibble: "97.179"), :valid?
+ assert_not_predicate klass.new(wibble: 97.179), :valid?
+ assert_not_predicate klass.new(wibble: BigDecimal("97.179")), :valid?
end
def test_acceptance_validator_doesnt_require_db_connection
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index 578881f754..60ebdce178 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -96,7 +96,7 @@ class YamlSerializationTest < ActiveRecord::TestCase
def test_deserializing_rails_41_yaml
topic = YAML.load(yaml_fixture("rails_4_1"))
- assert topic.new_record?
+ assert_predicate topic, :new_record?
assert_nil topic.id
assert_equal "The First Topic", topic.title
assert_equal({ omg: :lol }, topic.content)
@@ -105,7 +105,7 @@ class YamlSerializationTest < ActiveRecord::TestCase
def test_deserializing_rails_4_2_0_yaml
topic = YAML.load(yaml_fixture("rails_4_2_0"))
- assert_not topic.new_record?
+ assert_not_predicate topic, :new_record?
assert_equal 1, topic.id
assert_equal "The First Topic", topic.title
assert_equal("Have a nice day", topic.content)
diff --git a/activerecord/test/fixtures/.gitignore b/activerecord/test/fixtures/.gitignore
deleted file mode 100644
index 885029a512..0000000000
--- a/activerecord/test/fixtures/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.sqlite* \ No newline at end of file
diff --git a/activerecord/test/fixtures/customers.yml b/activerecord/test/fixtures/customers.yml
index 0399ff83b9..7d6c1366d0 100644
--- a/activerecord/test/fixtures/customers.yml
+++ b/activerecord/test/fixtures/customers.yml
@@ -23,4 +23,13 @@ barney:
address_street: Quiet Road
address_city: Peaceful Town
address_country: Tranquil Land
- gps_location: NULL \ No newline at end of file
+ gps_location: NULL
+
+mary:
+ id: 4
+ name: Mary
+ balance: 1
+ address_street: Funny Street
+ address_city: Peaceful Town
+ address_country: Nation Land
+ gps_location: NULL
diff --git a/activerecord/test/fixtures/minimalistics.yml b/activerecord/test/fixtures/minimalistics.yml
index c3ec546209..83df0551bc 100644
--- a/activerecord/test/fixtures/minimalistics.yml
+++ b/activerecord/test/fixtures/minimalistics.yml
@@ -1,2 +1,5 @@
+zero:
+ id: 0
+
first:
id: 1
diff --git a/activerecord/test/fixtures/other_posts.yml b/activerecord/test/fixtures/other_posts.yml
index 39ff763547..3e11a33802 100644
--- a/activerecord/test/fixtures/other_posts.yml
+++ b/activerecord/test/fixtures/other_posts.yml
@@ -5,3 +5,4 @@ second_welcome:
author_id: 1
title: Welcome to the another weblog
body: It's really nice today
+ comments_count: 1
diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml
index 86d46f753a..8d7e1e0ae7 100644
--- a/activerecord/test/fixtures/posts.yml
+++ b/activerecord/test/fixtures/posts.yml
@@ -28,6 +28,7 @@ sti_comments:
author_id: 1
title: sti comments
body: hello
+ comments_count: 5
type: Post
sti_post_and_comments:
@@ -35,6 +36,7 @@ sti_post_and_comments:
author_id: 1
title: sti me
body: hello
+ comments_count: 2
type: StiPost
sti_habtm:
@@ -50,6 +52,8 @@ eager_other:
title: eager loading with OR'd conditions
body: hello
type: Post
+ comments_count: 1
+ tags_count: 3
misc_by_bob:
id: 8
@@ -57,6 +61,7 @@ misc_by_bob:
title: misc post by bob
body: hello
type: Post
+ tags_count: 1
misc_by_mary:
id: 9
@@ -64,6 +69,7 @@ misc_by_mary:
title: misc post by mary
body: hello
type: Post
+ tags_count: 1
other_by_bob:
id: 10
@@ -71,6 +77,7 @@ other_by_bob:
title: other post by bob
body: hello
type: Post
+ tags_count: 1
other_by_mary:
id: 11
@@ -78,3 +85,4 @@ other_by_mary:
title: other post by mary
body: hello
type: Post
+ tags_count: 1
diff --git a/activerecord/test/fixtures/reserved_words/values.yml b/activerecord/test/fixtures/reserved_words/values.yml
index 7d109609ab..9ed9e5edc5 100644
--- a/activerecord/test/fixtures/reserved_words/values.yml
+++ b/activerecord/test/fixtures/reserved_words/values.yml
@@ -1,7 +1,7 @@
values1:
- id: 1
+ as: 1
group_id: 2
values2:
- id: 2
+ as: 2
group_id: 1
diff --git a/activerecord/test/fixtures/sponsors.yml b/activerecord/test/fixtures/sponsors.yml
index 2da541c539..02ddb8dd38 100644
--- a/activerecord/test/fixtures/sponsors.yml
+++ b/activerecord/test/fixtures/sponsors.yml
@@ -10,3 +10,6 @@ crazy_club_sponsor_for_groucho:
sponsor_club: crazy_club
sponsorable_id: 3
sponsorable_type: Member
+sponsor_for_author_david:
+ sponsorable_id: 1
+ sponsorable_type: Author
diff --git a/activerecord/test/fixtures/teapots.yml b/activerecord/test/fixtures/teapots.yml
deleted file mode 100644
index ff515beb45..0000000000
--- a/activerecord/test/fixtures/teapots.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-bob:
- id: 1
- name: Bob
diff --git a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb
index b892f50e41..7d4233fe31 100644
--- a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb
+++ b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb
@@ -5,7 +5,7 @@ class GiveMeBigNumbers < ActiveRecord::Migration::Current
create_table :big_numbers do |table|
table.column :bank_balance, :decimal, precision: 10, scale: 2
table.column :big_bank_balance, :decimal, precision: 15, scale: 2
- table.column :world_population, :decimal, precision: 10
+ table.column :world_population, :decimal, precision: 20
table.column :my_house_population, :decimal, precision: 2
table.column :value_of_e, :decimal
end
diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb
index abb5cb28e7..3f55364510 100644
--- a/activerecord/test/models/admin/user.rb
+++ b/activerecord/test/models/admin/user.rb
@@ -19,6 +19,9 @@ class Admin::User < ActiveRecord::Base
store :params, accessors: [ :token ], coder: YAML
store :settings, accessors: [ :color, :homepage ]
store_accessor :settings, :favorite_food
+ store :parent, accessors: [:birthday, :name], prefix: true
+ store :spouse, accessors: [:birthday], prefix: :partner
+ store_accessor :spouse, :name, prefix: :partner
store :preferences, accessors: [ :remember_login ]
store :json_data, accessors: [ :height, :weight ], coder: Coder.new
store :json_data_empty, accessors: [ :is_a_good_guy ], coder: Coder.new
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 3371fcbfcc..75932c7eb6 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -8,6 +8,7 @@ class Author < ActiveRecord::Base
has_many :posts_with_comments, -> { includes(:comments) }, class_name: "Post"
has_many :popular_grouped_posts, -> { includes(:comments).group("type").having("SUM(comments_count) > 1").select("type") }, class_name: "Post"
has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order("comments.id") }, class_name: "Post"
+ has_many :posts_sorted_by_id, -> { order(:id) }, class_name: "Post"
has_many :posts_sorted_by_id_limited, -> { order("posts.id").limit(1) }, class_name: "Post"
has_many :posts_with_categories, -> { includes(:categories) }, class_name: "Post"
has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, class_name: "Post"
@@ -22,6 +23,7 @@ class Author < ActiveRecord::Base
has_many :comments_containing_the_letter_e, through: :posts, source: :comments
has_many :comments_with_order_and_conditions, -> { order("comments.body").where("comments.body like 'Thank%'") }, through: :posts, source: :comments
has_many :comments_with_include, -> { includes(:post).where(posts: { type: "Post" }) }, through: :posts, source: :comments
+ has_many :comments_for_first_author, -> { for_first_author }, through: :posts, source: :comments
has_many :first_posts
has_many :comments_on_first_posts, -> { order("posts.id desc, comments.id asc") }, through: :first_posts, source: :comments
@@ -39,7 +41,7 @@ class Author < ActiveRecord::Base
-> { where(title: "Welcome to the weblog").where(Post.arel_table[:comments_count].gt(0)) },
class_name: "Post"
- has_many :comments_desc, -> { order("comments.id DESC") }, through: :posts, source: :comments
+ has_many :comments_desc, -> { order("comments.id DESC") }, through: :posts_sorted_by_id, source: :comments
has_many :unordered_comments, -> { unscope(:order).distinct }, through: :posts_sorted_by_id_limited, source: :comments
has_many :funky_comments, through: :posts, source: :comments
has_many :ordered_uniq_comments, -> { distinct.order("comments.id") }, through: :posts, source: :comments
@@ -87,6 +89,9 @@ class Author < ActiveRecord::Base
has_many :special_categories, through: :special_categorizations, source: :category
has_one :special_category, through: :special_categorizations, source: :category
+ has_many :special_categories_with_conditions, -> { where(categorizations: { special: true }) }, through: :categorizations, source: :category
+ has_many :nonspecial_categories_with_conditions, -> { where(categorizations: { special: false }) }, through: :categorizations, source: :category
+
has_many :categories_like_general, -> { where(name: "General") }, through: :categorizations, source: :category, class_name: "Category"
has_many :categorized_posts, through: :categorizations, source: :post
@@ -100,10 +105,12 @@ class Author < ActiveRecord::Base
has_many :taggings, through: :posts, source: :taggings
has_many :taggings_2, through: :posts, source: :tagging
has_many :tags, through: :posts
+ has_many :ordered_tags, through: :posts
has_many :post_categories, through: :posts, source: :categories
has_many :tagging_tags, through: :taggings, source: :tag
has_many :similar_posts, -> { distinct }, through: :tags, source: :tagged_posts
+ has_many :ordered_posts, -> { distinct }, through: :ordered_tags, source: :tagged_posts
has_many :distinct_tags, -> { select("DISTINCT tags.*").order("tags.name") }, through: :posts, source: :tags
has_many :tags_with_primary_key, through: :posts
@@ -155,6 +162,9 @@ class Author < ActiveRecord::Base
def extension_method; end
end
+ has_many :top_posts, -> { order(id: :asc) }, class_name: "Post"
+ has_many :other_top_posts, -> { order(id: :asc) }, class_name: "Post"
+
attr_accessor :post_log
after_initialize :set_post_log
diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb
index 740aa593ac..f0f0576709 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -60,6 +60,10 @@ end
class SpecialComment < Comment
default_scope { where(deleted_at: nil) }
+
+ def self.what_are_you
+ "a special comment..."
+ end
end
class SubSpecialComment < SpecialComment
@@ -72,7 +76,7 @@ class CommentThatAutomaticallyAltersPostBody < Comment
belongs_to :post, class_name: "PostThatLoadsCommentsInAnAfterSaveHook", foreign_key: :post_id
after_save do |comment|
- comment.post.update_attributes(body: "Automatically altered")
+ comment.post.update(body: "Automatically altered")
end
end
@@ -83,6 +87,6 @@ end
class CommentWithAfterCreateUpdate < Comment
after_create do
- update_attributes(body: "bar")
+ update(body: "bar")
end
end
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index bbc5fc2b2d..fc6488f729 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -87,6 +87,8 @@ class Firm < Company
has_many :association_with_references, -> { references(:foo) }, class_name: "Client"
+ has_many :developers_with_select, -> { select("id, name, first_name") }, class_name: "Developer"
+
has_one :lead_developer, class_name: "Developer"
has_many :projects
diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb
index 9454217e8d..f273badd85 100644
--- a/activerecord/test/models/contract.rb
+++ b/activerecord/test/models/contract.rb
@@ -2,7 +2,7 @@
class Contract < ActiveRecord::Base
belongs_to :company
- belongs_to :developer
+ belongs_to :developer, primary_key: :id
belongs_to :firm, foreign_key: "company_id"
before_save :hi
diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb
index 524a9d7bd9..bc501a5ce0 100644
--- a/activerecord/test/models/customer.rb
+++ b/activerecord/test/models/customer.rb
@@ -4,7 +4,7 @@ class Customer < ActiveRecord::Base
cattr_accessor :gps_conversion_was_run
composed_of :address, mapping: [ %w(address_street street), %w(address_city city), %w(address_country country) ], allow_nil: true
- composed_of :balance, class_name: "Money", mapping: %w(balance amount), converter: Proc.new(&:to_money)
+ composed_of :balance, class_name: "Money", mapping: %w(balance amount)
composed_of :gps_location, allow_nil: true
composed_of :non_blank_gps_location, class_name: "GpsLocation", allow_nil: true, mapping: %w(gps_location gps_location),
converter: lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps) }
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 56aafca60b..8881c69368 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -87,6 +87,17 @@ class Developer < ActiveRecord::Base
private :track_instance_count
end
+class SubDeveloper < Developer
+end
+
+class SymbolIgnoredDeveloper < ActiveRecord::Base
+ self.table_name = "developers"
+ self.ignored_columns = [:first_name, :last_name]
+
+ attr_accessor :last_name
+ define_attribute_method "last_name"
+end
+
class AuditLog < ActiveRecord::Base
belongs_to :developer, validate: true
belongs_to :unvalidated_developer, class_name: "Developer"
diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb
index 1c407844c5..eb6701b84e 100644
--- a/activerecord/test/models/drink_designer.rb
+++ b/activerecord/test/models/drink_designer.rb
@@ -3,3 +3,6 @@
class DrinkDesigner < ActiveRecord::Base
has_one :chef, as: :employable
end
+
+class MocktailDesigner < DrinkDesigner
+end
diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb
index 796aaa4dc9..e900fd40fb 100644
--- a/activerecord/test/models/face.rb
+++ b/activerecord/test/models/face.rb
@@ -2,10 +2,15 @@
class Face < ActiveRecord::Base
belongs_to :man, inverse_of: :face
+ belongs_to :human, polymorphic: true
belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_face
# Oracle identifier length is limited to 30 bytes or less, `polymorphic` renamed `poly`
belongs_to :poly_man_without_inverse, polymorphic: true
# These is a "broken" inverse_of for the purposes of testing
belongs_to :horrible_man, class_name: "Man", inverse_of: :horrible_face
belongs_to :horrible_polymorphic_man, polymorphic: true, inverse_of: :horrible_polymorphic_face
+
+ validate do
+ man
+ end
end
diff --git a/activerecord/test/models/frog.rb b/activerecord/test/models/frog.rb
new file mode 100644
index 0000000000..73601aacdd
--- /dev/null
+++ b/activerecord/test/models/frog.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class Frog < ActiveRecord::Base
+ after_save do
+ with_lock do
+ end
+ end
+end
diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb
index 3acd89a48e..e26920e951 100644
--- a/activerecord/test/models/man.rb
+++ b/activerecord/test/models/man.rb
@@ -11,3 +11,6 @@ class Man < ActiveRecord::Base
has_many :secret_interests, class_name: "Interest", inverse_of: :secret_man
has_one :mixed_case_monkey
end
+
+class Human < Man
+end
diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb
index 87f7aab9a2..e121a849d0 100644
--- a/activerecord/test/models/member_detail.rb
+++ b/activerecord/test/models/member_detail.rb
@@ -5,6 +5,7 @@ class MemberDetail < ActiveRecord::Base
belongs_to :organization
has_one :member_type, through: :member
has_one :membership, through: :member
+ has_one :admittable, through: :member, source_type: "Member"
has_many :organization_member_details, through: :organization, source: :member_details
end
diff --git a/activerecord/test/models/mocktail_designer.rb b/activerecord/test/models/mocktail_designer.rb
deleted file mode 100644
index 123ff4fb3d..0000000000
--- a/activerecord/test/models/mocktail_designer.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-class MocktailDesigner < DrinkDesigner
-end
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 4c8e847354..54eb5e6783 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -21,7 +21,7 @@ class Post < ActiveRecord::Base
scope :containing_the_letter_a, -> { where("body LIKE '%a%'") }
scope :titled_with_an_apostrophe, -> { where("title LIKE '%''%'") }
- scope :ranked_by_comments, -> { order("comments_count DESC") }
+ scope :ranked_by_comments, -> { order(arel_attribute(:comments_count).desc) }
scope :limit_by, lambda { |l| limit(l) }
scope :locked, -> { lock }
@@ -106,6 +106,9 @@ class Post < ActiveRecord::Base
end
end
+ has_many :indestructible_taggings, as: :taggable, counter_cache: :indestructible_tags_count
+ has_many :indestructible_tags, through: :indestructible_taggings, source: :tag
+
has_many :taggings_with_delete_all, class_name: "Tagging", as: :taggable, dependent: :delete_all, counter_cache: :taggings_with_delete_all_count
has_many :taggings_with_destroy, class_name: "Tagging", as: :taggable, dependent: :destroy, counter_cache: :taggings_with_destroy_count
@@ -115,6 +118,7 @@ class Post < ActiveRecord::Base
has_many :misc_tags, -> { where tags: { name: "Misc" } }, through: :taggings, source: :tag
has_many :funky_tags, through: :taggings, source: :tag
has_many :super_tags, through: :taggings
+ has_many :ordered_tags, through: :taggings
has_many :tags_with_primary_key, through: :taggings, source: :tag_with_primary_key
has_one :tagging, as: :taggable
@@ -318,5 +322,17 @@ class FakeKlass
def arel_attribute(name, table)
table[name]
end
+
+ def enforce_raw_sql_whitelist(*args)
+ # noop
+ end
+
+ def arel_table
+ Post.arel_table
+ end
+
+ def predicate_builder
+ Post.predicate_builder
+ end
end
end
diff --git a/activerecord/test/models/sponsor.rb b/activerecord/test/models/sponsor.rb
index f190860fd1..18ff103ffe 100644
--- a/activerecord/test/models/sponsor.rb
+++ b/activerecord/test/models/sponsor.rb
@@ -3,6 +3,7 @@
class Sponsor < ActiveRecord::Base
belongs_to :sponsor_club, class_name: "Club", foreign_key: "club_id"
belongs_to :sponsorable, polymorphic: true
+ belongs_to :sponsor, polymorphic: true
belongs_to :thing, polymorphic: true, foreign_type: :sponsorable_type, foreign_key: :sponsorable_id
belongs_to :sponsorable_with_conditions, -> { where name: "Ernie" }, polymorphic: true,
foreign_type: "sponsorable_type", foreign_key: "sponsorable_id"
diff --git a/activerecord/test/models/tag.rb b/activerecord/test/models/tag.rb
index 4495ac4a09..c1a8890a8a 100644
--- a/activerecord/test/models/tag.rb
+++ b/activerecord/test/models/tag.rb
@@ -11,5 +11,6 @@ end
class OrderedTag < Tag
self.table_name = "tags"
- has_many :taggings, -> { order("taggings.id DESC") }, foreign_key: "tag_id"
+ has_many :ordered_taggings, -> { order("taggings.id DESC") }, foreign_key: "tag_id", class_name: "Tagging"
+ has_many :tagged_posts, through: :ordered_taggings, source: "taggable", source_type: "Post"
end
diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb
index fc0af026c5..6d4230f6f4 100644
--- a/activerecord/test/models/tagging.rb
+++ b/activerecord/test/models/tagging.rb
@@ -8,8 +8,13 @@ class Tagging < ActiveRecord::Base
belongs_to :tag, -> { includes(:tagging) }
belongs_to :super_tag, class_name: "Tag", foreign_key: "super_tag_id"
belongs_to :invalid_tag, class_name: "Tag", foreign_key: "tag_id"
+ belongs_to :ordered_tag, class_name: "OrderedTag", foreign_key: "tag_id"
belongs_to :blue_tag, -> { where tags: { name: "Blue" } }, class_name: "Tag", foreign_key: :tag_id
belongs_to :tag_with_primary_key, class_name: "Tag", foreign_key: :tag_id, primary_key: :custom_primary_key
belongs_to :taggable, polymorphic: true, counter_cache: :tags_count
has_many :things, through: :taggable
end
+
+class IndestructibleTagging < Tagging
+ before_destroy { throw :abort }
+end
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 2154b50ef7..fa50eeb6a4 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -12,9 +12,17 @@ class Topic < ActiveRecord::Base
scope :scope_with_lambda, lambda { all }
+ scope :by_private_lifo, -> { where(author_name: private_lifo) }
scope :by_lifo, -> { where(author_name: "lifo") }
scope :replied, -> { where "replies_count > 0" }
+ class << self
+ private
+ def private_lifo
+ "lifo"
+ end
+ end
+
scope "approved_as_string", -> { where(approved: true) }
scope :anonymous_extension, -> {} do
def one
@@ -65,6 +73,9 @@ class Topic < ActiveRecord::Base
after_initialize :set_email_address
+ attr_accessor :change_approved_before_save
+ before_save :change_approved_callback
+
class_attribute :after_initialize_called
after_initialize do
self.class.after_initialize_called = true
@@ -96,6 +107,10 @@ class Topic < ActiveRecord::Base
def before_destroy_for_transaction; end
def after_save_for_transaction; end
def after_create_for_transaction; end
+
+ def change_approved_callback
+ self.approved = change_approved_before_save unless change_approved_before_save.nil?
+ end
end
class ImportantTopic < Topic
diff --git a/activerecord/test/models/wheel.rb b/activerecord/test/models/wheel.rb
index e05fb64477..8db57d181e 100644
--- a/activerecord/test/models/wheel.rb
+++ b/activerecord/test/models/wheel.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class Wheel < ActiveRecord::Base
- belongs_to :wheelable, polymorphic: true, counter_cache: true
+ belongs_to :wheelable, polymorphic: true, counter_cache: true, touch: true
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 8f872c38ba..92ad25ef76 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -21,6 +21,8 @@ ActiveRecord::Schema.define do
create_table :admin_users, force: true do |t|
t.string :name
t.string :settings, null: true, limit: 1024
+ t.string :parent, null: true, limit: 1024
+ t.string :spouse, null: true, limit: 1024
# MySQL does not allow default values for blobs. Fake it out with a
# big varchar below.
t.string :preferences, null: true, default: "", limit: 1024
@@ -123,7 +125,7 @@ ActiveRecord::Schema.define do
create_table :cars, force: true do |t|
t.string :name
t.integer :engines_count
- t.integer :wheels_count
+ t.integer :wheels_count, default: 0
t.column :lock_version, :integer, null: false, default: 0
t.timestamps null: false
end
@@ -191,6 +193,7 @@ ActiveRecord::Schema.define do
t.string :resource_id
t.string :resource_type
t.integer :developer_id
+ t.datetime :updated_at
t.datetime :deleted_at
t.integer :comments
end
@@ -204,10 +207,12 @@ ActiveRecord::Schema.define do
t.bigint :rating, default: 1
t.integer :account_id
t.string :description, default: ""
+ t.index [:name, :rating], order: :desc
+ t.index [:name, :description], length: 10
t.index [:firm_id, :type, :rating], name: "company_index", length: { type: 10 }, order: { rating: :desc }
t.index [:firm_id, :type], name: "company_partial_index", where: "(rating > 10)"
t.index :name, name: "company_name_index", using: :btree
- t.index "lower(name)", name: "company_expression_index" if supports_expression_index?
+ t.index "(CASE WHEN rating > 0 THEN lower(name) END)", name: "company_expression_index" if supports_expression_index?
end
create_table :content, force: true do |t|
@@ -342,6 +347,10 @@ ActiveRecord::Schema.define do
t.string :token
end
+ create_table :frogs, force: true do |t|
+ t.string :name
+ end
+
create_table :funny_jokes, force: true do |t|
t.string :name
end
@@ -477,7 +486,8 @@ ActiveRecord::Schema.define do
create_table :members, force: true do |t|
t.string :name
- t.integer :member_type_id
+ t.references :member_type, index: false
+ t.references :admittable, polymorphic: true, index: false
end
create_table :member_details, force: true do |t|
@@ -544,7 +554,7 @@ ActiveRecord::Schema.define do
create_table :numeric_data, force: true do |t|
t.decimal :bank_balance, precision: 10, scale: 2
t.decimal :big_bank_balance, precision: 15, scale: 2
- t.decimal :world_population, precision: 10, scale: 0
+ t.decimal :world_population, precision: 20, scale: 0
t.decimal :my_house_population, precision: 2, scale: 0
t.decimal :decimal_number_with_default, precision: 3, scale: 2, default: 2.78
t.float :temperature
@@ -687,6 +697,7 @@ ActiveRecord::Schema.define do
t.integer :taggings_with_delete_all_count, default: 0
t.integer :taggings_with_destroy_count, default: 0
t.integer :tags_count, default: 0
+ t.integer :indestructible_tags_count, default: 0
t.integer :tags_with_destroy_count, default: 0
t.integer :tags_with_nullify_count, default: 0
end
@@ -810,6 +821,7 @@ ActiveRecord::Schema.define do
create_table :sponsors, force: true do |t|
t.integer :club_id
t.references :sponsorable, polymorphic: true, index: false
+ t.references :sponsor, polymorphic: true, index: false
end
create_table :string_key_objects, id: false, force: true do |t|
@@ -844,6 +856,7 @@ ActiveRecord::Schema.define do
t.column :taggable_type, :string
t.column :taggable_id, :integer
t.string :comment
+ t.string :type
end
create_table :tasks, force: true do |t|
@@ -946,6 +959,7 @@ ActiveRecord::Schema.define do
t.string :poly_man_without_inverse_type
t.integer :horrible_polymorphic_man_id
t.string :horrible_polymorphic_man_type
+ t.references :human, polymorphic: true, index: false
end
create_table :interests, force: true do |t|
@@ -961,6 +975,7 @@ ActiveRecord::Schema.define do
end
create_table :wheels, force: true do |t|
+ t.integer :size
t.references :wheelable, polymorphic: true
end
diff --git a/activestorage/.babelrc b/activestorage/.babelrc
index a8211d329f..ed751f8745 100644
--- a/activestorage/.babelrc
+++ b/activestorage/.babelrc
@@ -1,5 +1,8 @@
{
"presets": [
["env", { "modules": false } ]
+ ],
+ "plugins": [
+ "external-helpers"
]
}
diff --git a/activestorage/.gitignore b/activestorage/.gitignore
index a532335bdd..3e78878ffc 100644
--- a/activestorage/.gitignore
+++ b/activestorage/.gitignore
@@ -1,6 +1,6 @@
-.byebug_history
-node_modules
-test/dummy/db/*.sqlite3
-test/dummy/db/*.sqlite3-journal
-test/dummy/log/*.log
-test/dummy/tmp/
+/src/
+/test/dummy/db/*.sqlite3
+/test/dummy/db/*.sqlite3-journal
+/test/dummy/log/*.log
+/test/dummy/tmp/
+/test/service/configurations.yml
diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md
index 358552313f..60d7d19540 100644
--- a/activestorage/CHANGELOG.md
+++ b/activestorage/CHANGELOG.md
@@ -1,3 +1,25 @@
-* Added to Rails.
+* Use the [ImageProcessing](https://github.com/janko-m/image_processing) gem
+ for Active Storage variants, and deprecate the MiniMagick backend.
- *DHH*
+ This means that variants are now automatically oriented if the original
+ image was rotated. Also, in addition to the existing ImageMagick
+ operations, variants can now use `:resize_to_fit`, `:resize_to_fill`, and
+ other ImageProcessing macros. These are now recommended over raw `:resize`,
+ as they also sharpen the thumbnail after resizing.
+
+ The ImageProcessing gem also comes with a backend implemented on
+ [libvips](http://jcupitt.github.io/libvips/), an alternative to
+ ImageMagick which has significantly better performance than
+ ImageMagick in most cases, both in terms of speed and memory usage. In
+ Active Storage it's now possible to switch to the libvips backend by
+ changing `Rails.application.config.active_storage.variant_processor` to
+ `:vips`.
+
+ *Janko Marohnić*
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
+
+
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activestorage/CHANGELOG.md) for previous changes.
diff --git a/activestorage/MIT-LICENSE b/activestorage/MIT-LICENSE
index 4e1c6cad79..eed89ac398 100644
--- a/activestorage/MIT-LICENSE
+++ b/activestorage/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2017 David Heinemeier Hansson, Basecamp
+Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activestorage/README.md b/activestorage/README.md
index 8814887950..b677721d95 100644
--- a/activestorage/README.md
+++ b/activestorage/README.md
@@ -1,10 +1,10 @@
# Active Storage
-Active Storage makes it simple to upload and reference files in cloud services like Amazon S3, Google Cloud Storage, or Microsoft Azure Storage, and attach those files to Active Records. Supports having one main service and mirrors in other services for redundancy. It also provides a disk service for testing or local deployments, but the focus is on cloud storage.
+Active Storage makes it simple to upload and reference files in cloud services like [Amazon S3](https://aws.amazon.com/s3/), [Google Cloud Storage](https://cloud.google.com/storage/docs/), or [Microsoft Azure Storage](https://azure.microsoft.com/en-us/services/storage/), and attach those files to Active Records. Supports having one main service and mirrors in other services for redundancy. It also provides a disk service for testing or local deployments, but the focus is on cloud storage.
Files can be uploaded from the server to the cloud or directly from the client to the cloud.
-Image files can furthermore be transformed using on-demand variants for quality, aspect ratio, size, or any other [MiniMagick](https://github.com/minimagick/minimagick) supported transformation.
+Image files can furthermore be transformed using on-demand variants for quality, aspect ratio, size, or any other [MiniMagick](https://github.com/minimagick/minimagick) or [Vips](http://www.rubydoc.info/gems/ruby-vips/Vips/Image) supported transformation.
## Compared to other storage solutions
@@ -12,6 +12,10 @@ A key difference to how Active Storage works compared to other attachment soluti
`Blob` models store attachment metadata (filename, content-type, etc.), and their identifier key in the storage service. Blob models do not store the actual binary data. They are intended to be immutable in spirit. One file, one blob. You can associate the same blob with multiple application models as well. And if you want to do transformations of a given `Blob`, the idea is that you'll simply create a new one, rather than attempt to mutate the existing one (though of course you can delete the previous version later if you don't need it).
+## Installation
+
+Run `rails active_storage:install` to copy over active_storage migrations.
+
## Examples
One attachment:
@@ -95,7 +99,7 @@ Variation of image attachment:
```erb
<%# Hitting the variant URL will lazy transform the original blob and then redirect to its new service location %>
-<%= image_tag user.avatar.variant(resize: "100x100") %>
+<%= image_tag user.avatar.variant(resize_to_fit: [100, 100]) %>
```
## Direct uploads
@@ -139,3 +143,17 @@ Active Storage, with its included JavaScript library, supports uploading directl
## License
Active Storage is released under the [MIT License](https://opensource.org/licenses/MIT).
+
+## Support
+
+API documentation is at:
+
+* http://api.rubyonrails.org
+
+Bug reports for the Ruby on Rails project can be filed here:
+
+* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
diff --git a/activestorage/Rakefile b/activestorage/Rakefile
index aa71a65f6e..7dc69e04ea 100644
--- a/activestorage/Rakefile
+++ b/activestorage/Rakefile
@@ -8,7 +8,10 @@ Rake::TestTask.new do |test|
test.libs << "app/controllers"
test.libs << "test"
test.test_files = FileList["test/**/*_test.rb"]
+ test.verbose = true
test.warning = false
end
+task :package
+
task default: :test
diff --git a/activestorage/activestorage.gemspec b/activestorage/activestorage.gemspec
index 911e1a0469..cb1bb00a25 100644
--- a/activestorage/activestorage.gemspec
+++ b/activestorage/activestorage.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Local and cloud file storage framework."
s.description = "Attach cloud and local files in Rails applications."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
s.email = "david@loudthinking.com"
s.homepage = "http://rubyonrails.org"
- s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*"]
+ s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*", "db/**/*"]
s.require_path = "lib"
s.metadata = {
@@ -27,4 +27,6 @@ Gem::Specification.new do |s|
s.add_dependency "actionpack", version
s.add_dependency "activerecord", version
+
+ s.add_dependency "marcel", "~> 0.3.1"
end
diff --git a/activestorage/app/assets/javascripts/activestorage.js b/activestorage/app/assets/javascripts/activestorage.js
index c1c0a2f6d9..a22f644238 100644
--- a/activestorage/app/assets/javascripts/activestorage.js
+++ b/activestorage/app/assets/javascripts/activestorage.js
@@ -1 +1,930 @@
-!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ActiveStorage=e():t.ActiveStorage=e()}(this,function(){return function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var r={};return e.m=t,e.c=r,e.d=function(t,r,n){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:n})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=2)}([function(t,e,r){"use strict";function n(t){var e=a(document.head,'meta[name="'+t+'"]');if(e)return e.getAttribute("content")}function i(t,e){return"string"==typeof t&&(e=t,t=document),o(t.querySelectorAll(e))}function a(t,e){return"string"==typeof t&&(e=t,t=document),t.querySelector(e)}function u(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=r.bubbles,i=r.cancelable,a=r.detail,u=document.createEvent("Event");return u.initEvent(e,n||!0,i||!0),u.detail=a||{},t.dispatchEvent(u),u}function o(t){return Array.isArray(t)?t:Array.from?Array.from(t):[].slice.call(t)}e.d=n,e.c=i,e.b=a,e.a=u,e.e=o},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if(t&&"function"==typeof t[e]){for(var r=arguments.length,n=Array(r>2?r-2:0),i=2;i<r;i++)n[i-2]=arguments[i];return t[e].apply(t,n)}}r.d(e,"a",function(){return c});var a=r(6),u=r(8),o=r(9),s=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),f=0,c=function(){function t(e,r,i){n(this,t),this.id=++f,this.file=e,this.url=r,this.delegate=i}return s(t,[{key:"create",value:function(t){var e=this;a.a.create(this.file,function(r,n){var a=new u.a(e.file,n,e.url);i(e.delegate,"directUploadWillCreateBlobWithXHR",a.xhr),a.create(function(r){if(r)t(r);else{var n=new o.a(a);i(e.delegate,"directUploadWillStoreFileWithXHR",n.xhr),n.create(function(e){e?t(e):t(null,a.toJSON())})}})})}}]),t}()},function(t,e,r){"use strict";function n(){window.ActiveStorage&&Object(i.a)()}Object.defineProperty(e,"__esModule",{value:!0});var i=r(3),a=r(1);r.d(e,"start",function(){return i.a}),r.d(e,"DirectUpload",function(){return a.a}),setTimeout(n,1)},function(t,e,r){"use strict";function n(){d||(d=!0,document.addEventListener("submit",i),document.addEventListener("ajax:before",a))}function i(t){u(t)}function a(t){"FORM"==t.target.tagName&&u(t)}function u(t){var e=t.target;if(e.hasAttribute(l))return void t.preventDefault();var r=new c.a(e),n=r.inputs;n.length&&(t.preventDefault(),e.setAttribute(l,""),n.forEach(s),r.start(function(t){e.removeAttribute(l),t?n.forEach(f):o(e)}))}function o(t){var e=Object(h.b)(t,"input[type=submit]");if(e){var r=e,n=r.disabled;e.disabled=!1,e.focus(),e.click(),e.disabled=n}else e=document.createElement("input"),e.type="submit",e.style="display:none",t.appendChild(e),e.click(),t.removeChild(e)}function s(t){t.disabled=!0}function f(t){t.disabled=!1}e.a=n;var c=r(4),h=r(0),l="data-direct-uploads-processing",d=!1},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(5),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o="input[type=file][data-direct-upload-url]:not([disabled])",s=function(){function t(e){n(this,t),this.form=e,this.inputs=Object(a.c)(e,o).filter(function(t){return t.files.length})}return u(t,[{key:"start",value:function(t){var e=this,r=this.createDirectUploadControllers();this.dispatch("start"),function n(){var i=r.shift();i?i.start(function(r){r?(t(r),e.dispatch("end")):n()}):(t(),e.dispatch("end"))}()}},{key:"createDirectUploadControllers",value:function(){var t=[];return this.inputs.forEach(function(e){Object(a.e)(e.files).forEach(function(r){var n=new i.a(e,r);t.push(n)})}),t}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return Object(a.a)(this.form,"direct-uploads:"+t,{detail:e})}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return o});var i=r(1),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=function(){function t(e,r){n(this,t),this.input=e,this.file=r,this.directUpload=new i.a(this.file,this.url,this),this.dispatch("initialize")}return u(t,[{key:"start",value:function(t){var e=this,r=document.createElement("input");r.type="hidden",r.name=this.input.name,this.input.insertAdjacentElement("beforebegin",r),this.dispatch("start"),this.directUpload.create(function(n,i){n?(r.parentNode.removeChild(r),e.dispatchError(n)):r.value=i.signed_id,e.dispatch("end"),t(n)})}},{key:"uploadRequestDidProgress",value:function(t){var e=t.loaded/t.total*100;e&&this.dispatch("progress",{progress:e})}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.file=this.file,e.id=this.directUpload.id,Object(a.a)(this.input,"direct-upload:"+t,{detail:e})}},{key:"dispatchError",value:function(t){this.dispatch("error",{error:t}).defaultPrevented||alert(t)}},{key:"directUploadWillCreateBlobWithXHR",value:function(t){this.dispatch("before-blob-request",{xhr:t})}},{key:"directUploadWillStoreFileWithXHR",value:function(t){var e=this;this.dispatch("before-storage-request",{xhr:t}),t.upload.addEventListener("progress",function(t){return e.uploadRequestDidProgress(t)})}},{key:"url",get:function(){return this.input.getAttribute("data-direct-upload-url")}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(7),a=r.n(i),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSlice,s=function(){function t(e){n(this,t),this.file=e,this.chunkSize=2097152,this.chunkCount=Math.ceil(this.file.size/this.chunkSize),this.chunkIndex=0}return u(t,null,[{key:"create",value:function(e,r){new t(e).create(r)}}]),u(t,[{key:"create",value:function(t){var e=this;this.callback=t,this.md5Buffer=new a.a.ArrayBuffer,this.fileReader=new FileReader,this.fileReader.addEventListener("load",function(t){return e.fileReaderDidLoad(t)}),this.fileReader.addEventListener("error",function(t){return e.fileReaderDidError(t)}),this.readNextChunk()}},{key:"fileReaderDidLoad",value:function(t){if(this.md5Buffer.append(t.target.result),!this.readNextChunk()){var e=this.md5Buffer.end(!0),r=btoa(e);this.callback(null,r)}}},{key:"fileReaderDidError",value:function(t){this.callback("Error reading "+this.file.name)}},{key:"readNextChunk",value:function(){if(this.chunkIndex<this.chunkCount){var t=this.chunkIndex*this.chunkSize,e=Math.min(t+this.chunkSize,this.file.size),r=o.call(this.file,t,e);return this.fileReader.readAsArrayBuffer(r),this.chunkIndex++,!0}return!1}}]),t}()},function(t,e,r){!function(e){t.exports=e()}(function(t){"use strict";function e(t,e){var r=t[0],n=t[1],i=t[2],a=t[3];r+=(n&i|~n&a)+e[0]-680876936|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[1]-389564586|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[2]+606105819|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[3]-1044525330|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[4]-176418897|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[5]+1200080426|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[6]-1473231341|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[7]-45705983|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[8]+1770035416|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[9]-1958414417|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[10]-42063|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[11]-1990404162|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[12]+1804603682|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[13]-40341101|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[14]-1502002290|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[15]+1236535329|0,n=(n<<22|n>>>10)+i|0,r+=(n&a|i&~a)+e[1]-165796510|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[6]-1069501632|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[11]+643717713|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[0]-373897302|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[5]-701558691|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[10]+38016083|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[15]-660478335|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[4]-405537848|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[9]+568446438|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[14]-1019803690|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[3]-187363961|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[8]+1163531501|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[13]-1444681467|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[2]-51403784|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[7]+1735328473|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[12]-1926607734|0,n=(n<<20|n>>>12)+i|0,r+=(n^i^a)+e[5]-378558|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[8]-2022574463|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[11]+1839030562|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[14]-35309556|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[1]-1530992060|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[4]+1272893353|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[7]-155497632|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[10]-1094730640|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[13]+681279174|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[0]-358537222|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[3]-722521979|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[6]+76029189|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[9]-640364487|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[12]-421815835|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[15]+530742520|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[2]-995338651|0,n=(n<<23|n>>>9)+i|0,r+=(i^(n|~a))+e[0]-198630844|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[7]+1126891415|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[14]-1416354905|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[5]-57434055|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[12]+1700485571|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[3]-1894986606|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[10]-1051523|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[1]-2054922799|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[8]+1873313359|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[15]-30611744|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[6]-1560198380|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[13]+1309151649|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[4]-145523070|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[11]-1120210379|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[2]+718787259|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[9]-343485551|0,n=(n<<21|n>>>11)+i|0,t[0]=r+t[0]|0,t[1]=n+t[1]|0,t[2]=i+t[2]|0,t[3]=a+t[3]|0}function r(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return r}function n(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t[e]+(t[e+1]<<8)+(t[e+2]<<16)+(t[e+3]<<24);return r}function i(t){var n,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(n=64;n<=f;n+=64)e(c,r(t.substring(n-64,n)));for(t=t.substring(n-64),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],n=0;n<i;n+=1)a[n>>2]|=t.charCodeAt(n)<<(n%4<<3);if(a[n>>2]|=128<<(n%4<<3),n>55)for(e(c,a),n=0;n<16;n+=1)a[n]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function a(t){var r,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(r=64;r<=f;r+=64)e(c,n(t.subarray(r-64,r)));for(t=r-64<f?t.subarray(r-64):new Uint8Array(0),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],r=0;r<i;r+=1)a[r>>2]|=t[r]<<(r%4<<3);if(a[r>>2]|=128<<(r%4<<3),r>55)for(e(c,a),r=0;r<16;r+=1)a[r]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function u(t){var e,r="";for(e=0;e<4;e+=1)r+=p[t>>8*e+4&15]+p[t>>8*e&15];return r}function o(t){var e;for(e=0;e<t.length;e+=1)t[e]=u(t[e]);return t.join("")}function s(t){return/[\u0080-\uFFFF]/.test(t)&&(t=unescape(encodeURIComponent(t))),t}function f(t,e){var r,n=t.length,i=new ArrayBuffer(n),a=new Uint8Array(i);for(r=0;r<n;r+=1)a[r]=t.charCodeAt(r);return e?a:i}function c(t){return String.fromCharCode.apply(null,new Uint8Array(t))}function h(t,e,r){var n=new Uint8Array(t.byteLength+e.byteLength);return n.set(new Uint8Array(t)),n.set(new Uint8Array(e),t.byteLength),r?n:n.buffer}function l(t){var e,r=[],n=t.length;for(e=0;e<n-1;e+=2)r.push(parseInt(t.substr(e,2),16));return String.fromCharCode.apply(String,r)}function d(){this.reset()}var p=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];return"5d41402abc4b2a76b9719d911017c592"!==o(i("hello"))&&function(t,e){var r=(65535&t)+(65535&e);return(t>>16)+(e>>16)+(r>>16)<<16|65535&r},"undefined"==typeof ArrayBuffer||ArrayBuffer.prototype.slice||function(){function e(t,e){return t=0|t||0,t<0?Math.max(t+e,0):Math.min(t,e)}ArrayBuffer.prototype.slice=function(r,n){var i,a,u,o,s=this.byteLength,f=e(r,s),c=s;return n!==t&&(c=e(n,s)),f>c?new ArrayBuffer(0):(i=c-f,a=new ArrayBuffer(i),u=new Uint8Array(a),o=new Uint8Array(this,f,i),u.set(o),a)}}(),d.prototype.append=function(t){return this.appendBinary(s(t)),this},d.prototype.appendBinary=function(t){this._buff+=t,this._length+=t.length;var n,i=this._buff.length;for(n=64;n<=i;n+=64)e(this._hash,r(this._buff.substring(n-64,n)));return this._buff=this._buff.substring(n-64),this},d.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n.charCodeAt(e)<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.prototype.reset=function(){return this._buff="",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash}},d.prototype.setState=function(t){return this._buff=t.buff,this._length=t.length,this._hash=t.hash,this},d.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},d.prototype._finish=function(t,r){var n,i,a,u=r;if(t[u>>2]|=128<<(u%4<<3),u>55)for(e(this._hash,t),u=0;u<16;u+=1)t[u]=0;n=8*this._length,n=n.toString(16).match(/(.*?)(.{0,8})$/),i=parseInt(n[2],16),a=parseInt(n[1],16)||0,t[14]=i,t[15]=a,e(this._hash,t)},d.hash=function(t,e){return d.hashBinary(s(t),e)},d.hashBinary=function(t,e){var r=i(t),n=o(r);return e?l(n):n},d.ArrayBuffer=function(){this.reset()},d.ArrayBuffer.prototype.append=function(t){var r,i=h(this._buff.buffer,t,!0),a=i.length;for(this._length+=t.byteLength,r=64;r<=a;r+=64)e(this._hash,n(i.subarray(r-64,r)));return this._buff=r-64<a?new Uint8Array(i.buffer.slice(r-64)):new Uint8Array(0),this},d.ArrayBuffer.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n[e]<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.ArrayBuffer.prototype.getState=function(){var t=d.prototype.getState.call(this);return t.buff=c(t.buff),t},d.ArrayBuffer.prototype.setState=function(t){return t.buff=f(t.buff,!0),d.prototype.setState.call(this,t)},d.ArrayBuffer.prototype.destroy=d.prototype.destroy,d.ArrayBuffer.prototype._finish=d.prototype._finish,d.ArrayBuffer.hash=function(t,e){var r=a(new Uint8Array(t)),n=o(r);return e?l(n):n},d})},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return u});var i=r(0),a=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),u=function(){function t(e,r,a){var u=this;n(this,t),this.file=e,this.attributes={filename:e.name,content_type:e.type,byte_size:e.size,checksum:r},this.xhr=new XMLHttpRequest,this.xhr.open("POST",a,!0),this.xhr.responseType="json",this.xhr.setRequestHeader("Content-Type","application/json"),this.xhr.setRequestHeader("Accept","application/json"),this.xhr.setRequestHeader("X-Requested-With","XMLHttpRequest"),this.xhr.setRequestHeader("X-CSRF-Token",Object(i.d)("csrf-token")),this.xhr.addEventListener("load",function(t){return u.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return u.requestDidError(t)})}return a(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(JSON.stringify({blob:this.attributes}))}},{key:"requestDidLoad",value:function(t){var e=this.xhr,r=e.status,n=e.response;if(r>=200&&r<300){var i=n.direct_upload;delete n.direct_upload,this.attributes=n,this.directUploadData=i,this.callback(null,this.toJSON())}else this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error creating Blob for "'+this.file.name+'". Status: '+this.xhr.status)}},{key:"toJSON",value:function(){var t={};for(var e in this.attributes)t[e]=this.attributes[e];return t}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return a});var i=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),a=function(){function t(e){var r=this;n(this,t),this.blob=e,this.file=e.file;var i=e.directUploadData,a=i.url,u=i.headers;this.xhr=new XMLHttpRequest,this.xhr.open("PUT",a,!0),this.xhr.responseType="text";for(var o in u)this.xhr.setRequestHeader(o,u[o]);this.xhr.addEventListener("load",function(t){return r.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return r.requestDidError(t)})}return i(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(this.file)}},{key:"requestDidLoad",value:function(t){var e=this.xhr,r=e.status,n=e.response;r>=200&&r<300?this.callback(null,n):this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error storing "'+this.file.name+'". Status: '+this.xhr.status)}}]),t}()}])}); \ No newline at end of file
+(function(global, factory) {
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : factory(global.ActiveStorage = {});
+})(this, function(exports) {
+ "use strict";
+ function createCommonjsModule(fn, module) {
+ return module = {
+ exports: {}
+ }, fn(module, module.exports), module.exports;
+ }
+ var sparkMd5 = createCommonjsModule(function(module, exports) {
+ (function(factory) {
+ {
+ module.exports = factory();
+ }
+ })(function(undefined) {
+ var hex_chr = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" ];
+ function md5cycle(x, k) {
+ var a = x[0], b = x[1], c = x[2], d = x[3];
+ a += (b & c | ~b & d) + k[0] - 680876936 | 0;
+ a = (a << 7 | a >>> 25) + b | 0;
+ d += (a & b | ~a & c) + k[1] - 389564586 | 0;
+ d = (d << 12 | d >>> 20) + a | 0;
+ c += (d & a | ~d & b) + k[2] + 606105819 | 0;
+ c = (c << 17 | c >>> 15) + d | 0;
+ b += (c & d | ~c & a) + k[3] - 1044525330 | 0;
+ b = (b << 22 | b >>> 10) + c | 0;
+ a += (b & c | ~b & d) + k[4] - 176418897 | 0;
+ a = (a << 7 | a >>> 25) + b | 0;
+ d += (a & b | ~a & c) + k[5] + 1200080426 | 0;
+ d = (d << 12 | d >>> 20) + a | 0;
+ c += (d & a | ~d & b) + k[6] - 1473231341 | 0;
+ c = (c << 17 | c >>> 15) + d | 0;
+ b += (c & d | ~c & a) + k[7] - 45705983 | 0;
+ b = (b << 22 | b >>> 10) + c | 0;
+ a += (b & c | ~b & d) + k[8] + 1770035416 | 0;
+ a = (a << 7 | a >>> 25) + b | 0;
+ d += (a & b | ~a & c) + k[9] - 1958414417 | 0;
+ d = (d << 12 | d >>> 20) + a | 0;
+ c += (d & a | ~d & b) + k[10] - 42063 | 0;
+ c = (c << 17 | c >>> 15) + d | 0;
+ b += (c & d | ~c & a) + k[11] - 1990404162 | 0;
+ b = (b << 22 | b >>> 10) + c | 0;
+ a += (b & c | ~b & d) + k[12] + 1804603682 | 0;
+ a = (a << 7 | a >>> 25) + b | 0;
+ d += (a & b | ~a & c) + k[13] - 40341101 | 0;
+ d = (d << 12 | d >>> 20) + a | 0;
+ c += (d & a | ~d & b) + k[14] - 1502002290 | 0;
+ c = (c << 17 | c >>> 15) + d | 0;
+ b += (c & d | ~c & a) + k[15] + 1236535329 | 0;
+ b = (b << 22 | b >>> 10) + c | 0;
+ a += (b & d | c & ~d) + k[1] - 165796510 | 0;
+ a = (a << 5 | a >>> 27) + b | 0;
+ d += (a & c | b & ~c) + k[6] - 1069501632 | 0;
+ d = (d << 9 | d >>> 23) + a | 0;
+ c += (d & b | a & ~b) + k[11] + 643717713 | 0;
+ c = (c << 14 | c >>> 18) + d | 0;
+ b += (c & a | d & ~a) + k[0] - 373897302 | 0;
+ b = (b << 20 | b >>> 12) + c | 0;
+ a += (b & d | c & ~d) + k[5] - 701558691 | 0;
+ a = (a << 5 | a >>> 27) + b | 0;
+ d += (a & c | b & ~c) + k[10] + 38016083 | 0;
+ d = (d << 9 | d >>> 23) + a | 0;
+ c += (d & b | a & ~b) + k[15] - 660478335 | 0;
+ c = (c << 14 | c >>> 18) + d | 0;
+ b += (c & a | d & ~a) + k[4] - 405537848 | 0;
+ b = (b << 20 | b >>> 12) + c | 0;
+ a += (b & d | c & ~d) + k[9] + 568446438 | 0;
+ a = (a << 5 | a >>> 27) + b | 0;
+ d += (a & c | b & ~c) + k[14] - 1019803690 | 0;
+ d = (d << 9 | d >>> 23) + a | 0;
+ c += (d & b | a & ~b) + k[3] - 187363961 | 0;
+ c = (c << 14 | c >>> 18) + d | 0;
+ b += (c & a | d & ~a) + k[8] + 1163531501 | 0;
+ b = (b << 20 | b >>> 12) + c | 0;
+ a += (b & d | c & ~d) + k[13] - 1444681467 | 0;
+ a = (a << 5 | a >>> 27) + b | 0;
+ d += (a & c | b & ~c) + k[2] - 51403784 | 0;
+ d = (d << 9 | d >>> 23) + a | 0;
+ c += (d & b | a & ~b) + k[7] + 1735328473 | 0;
+ c = (c << 14 | c >>> 18) + d | 0;
+ b += (c & a | d & ~a) + k[12] - 1926607734 | 0;
+ b = (b << 20 | b >>> 12) + c | 0;
+ a += (b ^ c ^ d) + k[5] - 378558 | 0;
+ a = (a << 4 | a >>> 28) + b | 0;
+ d += (a ^ b ^ c) + k[8] - 2022574463 | 0;
+ d = (d << 11 | d >>> 21) + a | 0;
+ c += (d ^ a ^ b) + k[11] + 1839030562 | 0;
+ c = (c << 16 | c >>> 16) + d | 0;
+ b += (c ^ d ^ a) + k[14] - 35309556 | 0;
+ b = (b << 23 | b >>> 9) + c | 0;
+ a += (b ^ c ^ d) + k[1] - 1530992060 | 0;
+ a = (a << 4 | a >>> 28) + b | 0;
+ d += (a ^ b ^ c) + k[4] + 1272893353 | 0;
+ d = (d << 11 | d >>> 21) + a | 0;
+ c += (d ^ a ^ b) + k[7] - 155497632 | 0;
+ c = (c << 16 | c >>> 16) + d | 0;
+ b += (c ^ d ^ a) + k[10] - 1094730640 | 0;
+ b = (b << 23 | b >>> 9) + c | 0;
+ a += (b ^ c ^ d) + k[13] + 681279174 | 0;
+ a = (a << 4 | a >>> 28) + b | 0;
+ d += (a ^ b ^ c) + k[0] - 358537222 | 0;
+ d = (d << 11 | d >>> 21) + a | 0;
+ c += (d ^ a ^ b) + k[3] - 722521979 | 0;
+ c = (c << 16 | c >>> 16) + d | 0;
+ b += (c ^ d ^ a) + k[6] + 76029189 | 0;
+ b = (b << 23 | b >>> 9) + c | 0;
+ a += (b ^ c ^ d) + k[9] - 640364487 | 0;
+ a = (a << 4 | a >>> 28) + b | 0;
+ d += (a ^ b ^ c) + k[12] - 421815835 | 0;
+ d = (d << 11 | d >>> 21) + a | 0;
+ c += (d ^ a ^ b) + k[15] + 530742520 | 0;
+ c = (c << 16 | c >>> 16) + d | 0;
+ b += (c ^ d ^ a) + k[2] - 995338651 | 0;
+ b = (b << 23 | b >>> 9) + c | 0;
+ a += (c ^ (b | ~d)) + k[0] - 198630844 | 0;
+ a = (a << 6 | a >>> 26) + b | 0;
+ d += (b ^ (a | ~c)) + k[7] + 1126891415 | 0;
+ d = (d << 10 | d >>> 22) + a | 0;
+ c += (a ^ (d | ~b)) + k[14] - 1416354905 | 0;
+ c = (c << 15 | c >>> 17) + d | 0;
+ b += (d ^ (c | ~a)) + k[5] - 57434055 | 0;
+ b = (b << 21 | b >>> 11) + c | 0;
+ a += (c ^ (b | ~d)) + k[12] + 1700485571 | 0;
+ a = (a << 6 | a >>> 26) + b | 0;
+ d += (b ^ (a | ~c)) + k[3] - 1894986606 | 0;
+ d = (d << 10 | d >>> 22) + a | 0;
+ c += (a ^ (d | ~b)) + k[10] - 1051523 | 0;
+ c = (c << 15 | c >>> 17) + d | 0;
+ b += (d ^ (c | ~a)) + k[1] - 2054922799 | 0;
+ b = (b << 21 | b >>> 11) + c | 0;
+ a += (c ^ (b | ~d)) + k[8] + 1873313359 | 0;
+ a = (a << 6 | a >>> 26) + b | 0;
+ d += (b ^ (a | ~c)) + k[15] - 30611744 | 0;
+ d = (d << 10 | d >>> 22) + a | 0;
+ c += (a ^ (d | ~b)) + k[6] - 1560198380 | 0;
+ c = (c << 15 | c >>> 17) + d | 0;
+ b += (d ^ (c | ~a)) + k[13] + 1309151649 | 0;
+ b = (b << 21 | b >>> 11) + c | 0;
+ a += (c ^ (b | ~d)) + k[4] - 145523070 | 0;
+ a = (a << 6 | a >>> 26) + b | 0;
+ d += (b ^ (a | ~c)) + k[11] - 1120210379 | 0;
+ d = (d << 10 | d >>> 22) + a | 0;
+ c += (a ^ (d | ~b)) + k[2] + 718787259 | 0;
+ c = (c << 15 | c >>> 17) + d | 0;
+ b += (d ^ (c | ~a)) + k[9] - 343485551 | 0;
+ b = (b << 21 | b >>> 11) + c | 0;
+ x[0] = a + x[0] | 0;
+ x[1] = b + x[1] | 0;
+ x[2] = c + x[2] | 0;
+ x[3] = d + x[3] | 0;
+ }
+ function md5blk(s) {
+ var md5blks = [], i;
+ for (i = 0; i < 64; i += 4) {
+ md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24);
+ }
+ return md5blks;
+ }
+ function md5blk_array(a) {
+ var md5blks = [], i;
+ for (i = 0; i < 64; i += 4) {
+ md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24);
+ }
+ return md5blks;
+ }
+ function md51(s) {
+ var n = s.length, state = [ 1732584193, -271733879, -1732584194, 271733878 ], i, length, tail, tmp, lo, hi;
+ for (i = 64; i <= n; i += 64) {
+ md5cycle(state, md5blk(s.substring(i - 64, i)));
+ }
+ s = s.substring(i - 64);
+ length = s.length;
+ tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
+ for (i = 0; i < length; i += 1) {
+ tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3);
+ }
+ tail[i >> 2] |= 128 << (i % 4 << 3);
+ if (i > 55) {
+ md5cycle(state, tail);
+ for (i = 0; i < 16; i += 1) {
+ tail[i] = 0;
+ }
+ }
+ tmp = n * 8;
+ tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
+ lo = parseInt(tmp[2], 16);
+ hi = parseInt(tmp[1], 16) || 0;
+ tail[14] = lo;
+ tail[15] = hi;
+ md5cycle(state, tail);
+ return state;
+ }
+ function md51_array(a) {
+ var n = a.length, state = [ 1732584193, -271733879, -1732584194, 271733878 ], i, length, tail, tmp, lo, hi;
+ for (i = 64; i <= n; i += 64) {
+ md5cycle(state, md5blk_array(a.subarray(i - 64, i)));
+ }
+ a = i - 64 < n ? a.subarray(i - 64) : new Uint8Array(0);
+ length = a.length;
+ tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
+ for (i = 0; i < length; i += 1) {
+ tail[i >> 2] |= a[i] << (i % 4 << 3);
+ }
+ tail[i >> 2] |= 128 << (i % 4 << 3);
+ if (i > 55) {
+ md5cycle(state, tail);
+ for (i = 0; i < 16; i += 1) {
+ tail[i] = 0;
+ }
+ }
+ tmp = n * 8;
+ tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
+ lo = parseInt(tmp[2], 16);
+ hi = parseInt(tmp[1], 16) || 0;
+ tail[14] = lo;
+ tail[15] = hi;
+ md5cycle(state, tail);
+ return state;
+ }
+ function rhex(n) {
+ var s = "", j;
+ for (j = 0; j < 4; j += 1) {
+ s += hex_chr[n >> j * 8 + 4 & 15] + hex_chr[n >> j * 8 & 15];
+ }
+ return s;
+ }
+ function hex(x) {
+ var i;
+ for (i = 0; i < x.length; i += 1) {
+ x[i] = rhex(x[i]);
+ }
+ return x.join("");
+ }
+ if (hex(md51("hello")) !== "5d41402abc4b2a76b9719d911017c592") ;
+ if (typeof ArrayBuffer !== "undefined" && !ArrayBuffer.prototype.slice) {
+ (function() {
+ function clamp(val, length) {
+ val = val | 0 || 0;
+ if (val < 0) {
+ return Math.max(val + length, 0);
+ }
+ return Math.min(val, length);
+ }
+ ArrayBuffer.prototype.slice = function(from, to) {
+ var length = this.byteLength, begin = clamp(from, length), end = length, num, target, targetArray, sourceArray;
+ if (to !== undefined) {
+ end = clamp(to, length);
+ }
+ if (begin > end) {
+ return new ArrayBuffer(0);
+ }
+ num = end - begin;
+ target = new ArrayBuffer(num);
+ targetArray = new Uint8Array(target);
+ sourceArray = new Uint8Array(this, begin, num);
+ targetArray.set(sourceArray);
+ return target;
+ };
+ })();
+ }
+ function toUtf8(str) {
+ if (/[\u0080-\uFFFF]/.test(str)) {
+ str = unescape(encodeURIComponent(str));
+ }
+ return str;
+ }
+ function utf8Str2ArrayBuffer(str, returnUInt8Array) {
+ var length = str.length, buff = new ArrayBuffer(length), arr = new Uint8Array(buff), i;
+ for (i = 0; i < length; i += 1) {
+ arr[i] = str.charCodeAt(i);
+ }
+ return returnUInt8Array ? arr : buff;
+ }
+ function arrayBuffer2Utf8Str(buff) {
+ return String.fromCharCode.apply(null, new Uint8Array(buff));
+ }
+ function concatenateArrayBuffers(first, second, returnUInt8Array) {
+ var result = new Uint8Array(first.byteLength + second.byteLength);
+ result.set(new Uint8Array(first));
+ result.set(new Uint8Array(second), first.byteLength);
+ return returnUInt8Array ? result : result.buffer;
+ }
+ function hexToBinaryString(hex) {
+ var bytes = [], length = hex.length, x;
+ for (x = 0; x < length - 1; x += 2) {
+ bytes.push(parseInt(hex.substr(x, 2), 16));
+ }
+ return String.fromCharCode.apply(String, bytes);
+ }
+ function SparkMD5() {
+ this.reset();
+ }
+ SparkMD5.prototype.append = function(str) {
+ this.appendBinary(toUtf8(str));
+ return this;
+ };
+ SparkMD5.prototype.appendBinary = function(contents) {
+ this._buff += contents;
+ this._length += contents.length;
+ var length = this._buff.length, i;
+ for (i = 64; i <= length; i += 64) {
+ md5cycle(this._hash, md5blk(this._buff.substring(i - 64, i)));
+ }
+ this._buff = this._buff.substring(i - 64);
+ return this;
+ };
+ SparkMD5.prototype.end = function(raw) {
+ var buff = this._buff, length = buff.length, i, tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], ret;
+ for (i = 0; i < length; i += 1) {
+ tail[i >> 2] |= buff.charCodeAt(i) << (i % 4 << 3);
+ }
+ this._finish(tail, length);
+ ret = hex(this._hash);
+ if (raw) {
+ ret = hexToBinaryString(ret);
+ }
+ this.reset();
+ return ret;
+ };
+ SparkMD5.prototype.reset = function() {
+ this._buff = "";
+ this._length = 0;
+ this._hash = [ 1732584193, -271733879, -1732584194, 271733878 ];
+ return this;
+ };
+ SparkMD5.prototype.getState = function() {
+ return {
+ buff: this._buff,
+ length: this._length,
+ hash: this._hash
+ };
+ };
+ SparkMD5.prototype.setState = function(state) {
+ this._buff = state.buff;
+ this._length = state.length;
+ this._hash = state.hash;
+ return this;
+ };
+ SparkMD5.prototype.destroy = function() {
+ delete this._hash;
+ delete this._buff;
+ delete this._length;
+ };
+ SparkMD5.prototype._finish = function(tail, length) {
+ var i = length, tmp, lo, hi;
+ tail[i >> 2] |= 128 << (i % 4 << 3);
+ if (i > 55) {
+ md5cycle(this._hash, tail);
+ for (i = 0; i < 16; i += 1) {
+ tail[i] = 0;
+ }
+ }
+ tmp = this._length * 8;
+ tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
+ lo = parseInt(tmp[2], 16);
+ hi = parseInt(tmp[1], 16) || 0;
+ tail[14] = lo;
+ tail[15] = hi;
+ md5cycle(this._hash, tail);
+ };
+ SparkMD5.hash = function(str, raw) {
+ return SparkMD5.hashBinary(toUtf8(str), raw);
+ };
+ SparkMD5.hashBinary = function(content, raw) {
+ var hash = md51(content), ret = hex(hash);
+ return raw ? hexToBinaryString(ret) : ret;
+ };
+ SparkMD5.ArrayBuffer = function() {
+ this.reset();
+ };
+ SparkMD5.ArrayBuffer.prototype.append = function(arr) {
+ var buff = concatenateArrayBuffers(this._buff.buffer, arr, true), length = buff.length, i;
+ this._length += arr.byteLength;
+ for (i = 64; i <= length; i += 64) {
+ md5cycle(this._hash, md5blk_array(buff.subarray(i - 64, i)));
+ }
+ this._buff = i - 64 < length ? new Uint8Array(buff.buffer.slice(i - 64)) : new Uint8Array(0);
+ return this;
+ };
+ SparkMD5.ArrayBuffer.prototype.end = function(raw) {
+ var buff = this._buff, length = buff.length, tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], i, ret;
+ for (i = 0; i < length; i += 1) {
+ tail[i >> 2] |= buff[i] << (i % 4 << 3);
+ }
+ this._finish(tail, length);
+ ret = hex(this._hash);
+ if (raw) {
+ ret = hexToBinaryString(ret);
+ }
+ this.reset();
+ return ret;
+ };
+ SparkMD5.ArrayBuffer.prototype.reset = function() {
+ this._buff = new Uint8Array(0);
+ this._length = 0;
+ this._hash = [ 1732584193, -271733879, -1732584194, 271733878 ];
+ return this;
+ };
+ SparkMD5.ArrayBuffer.prototype.getState = function() {
+ var state = SparkMD5.prototype.getState.call(this);
+ state.buff = arrayBuffer2Utf8Str(state.buff);
+ return state;
+ };
+ SparkMD5.ArrayBuffer.prototype.setState = function(state) {
+ state.buff = utf8Str2ArrayBuffer(state.buff, true);
+ return SparkMD5.prototype.setState.call(this, state);
+ };
+ SparkMD5.ArrayBuffer.prototype.destroy = SparkMD5.prototype.destroy;
+ SparkMD5.ArrayBuffer.prototype._finish = SparkMD5.prototype._finish;
+ SparkMD5.ArrayBuffer.hash = function(arr, raw) {
+ var hash = md51_array(new Uint8Array(arr)), ret = hex(hash);
+ return raw ? hexToBinaryString(ret) : ret;
+ };
+ return SparkMD5;
+ });
+ });
+ var classCallCheck = function(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ };
+ var createClass = function() {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+ return function(Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+ }();
+ var fileSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
+ var FileChecksum = function() {
+ createClass(FileChecksum, null, [ {
+ key: "create",
+ value: function create(file, callback) {
+ var instance = new FileChecksum(file);
+ instance.create(callback);
+ }
+ } ]);
+ function FileChecksum(file) {
+ classCallCheck(this, FileChecksum);
+ this.file = file;
+ this.chunkSize = 2097152;
+ this.chunkCount = Math.ceil(this.file.size / this.chunkSize);
+ this.chunkIndex = 0;
+ }
+ createClass(FileChecksum, [ {
+ key: "create",
+ value: function create(callback) {
+ var _this = this;
+ this.callback = callback;
+ this.md5Buffer = new sparkMd5.ArrayBuffer();
+ this.fileReader = new FileReader();
+ this.fileReader.addEventListener("load", function(event) {
+ return _this.fileReaderDidLoad(event);
+ });
+ this.fileReader.addEventListener("error", function(event) {
+ return _this.fileReaderDidError(event);
+ });
+ this.readNextChunk();
+ }
+ }, {
+ key: "fileReaderDidLoad",
+ value: function fileReaderDidLoad(event) {
+ this.md5Buffer.append(event.target.result);
+ if (!this.readNextChunk()) {
+ var binaryDigest = this.md5Buffer.end(true);
+ var base64digest = btoa(binaryDigest);
+ this.callback(null, base64digest);
+ }
+ }
+ }, {
+ key: "fileReaderDidError",
+ value: function fileReaderDidError(event) {
+ this.callback("Error reading " + this.file.name);
+ }
+ }, {
+ key: "readNextChunk",
+ value: function readNextChunk() {
+ if (this.chunkIndex < this.chunkCount) {
+ var start = this.chunkIndex * this.chunkSize;
+ var end = Math.min(start + this.chunkSize, this.file.size);
+ var bytes = fileSlice.call(this.file, start, end);
+ this.fileReader.readAsArrayBuffer(bytes);
+ this.chunkIndex++;
+ return true;
+ } else {
+ return false;
+ }
+ }
+ } ]);
+ return FileChecksum;
+ }();
+ function getMetaValue(name) {
+ var element = findElement(document.head, 'meta[name="' + name + '"]');
+ if (element) {
+ return element.getAttribute("content");
+ }
+ }
+ function findElements(root, selector) {
+ if (typeof root == "string") {
+ selector = root;
+ root = document;
+ }
+ var elements = root.querySelectorAll(selector);
+ return toArray$1(elements);
+ }
+ function findElement(root, selector) {
+ if (typeof root == "string") {
+ selector = root;
+ root = document;
+ }
+ return root.querySelector(selector);
+ }
+ function dispatchEvent(element, type) {
+ var eventInit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+ var disabled = element.disabled;
+ var bubbles = eventInit.bubbles, cancelable = eventInit.cancelable, detail = eventInit.detail;
+ var event = document.createEvent("Event");
+ event.initEvent(type, bubbles || true, cancelable || true);
+ event.detail = detail || {};
+ try {
+ element.disabled = false;
+ element.dispatchEvent(event);
+ } finally {
+ element.disabled = disabled;
+ }
+ return event;
+ }
+ function toArray$1(value) {
+ if (Array.isArray(value)) {
+ return value;
+ } else if (Array.from) {
+ return Array.from(value);
+ } else {
+ return [].slice.call(value);
+ }
+ }
+ var BlobRecord = function() {
+ function BlobRecord(file, checksum, url) {
+ var _this = this;
+ classCallCheck(this, BlobRecord);
+ this.file = file;
+ this.attributes = {
+ filename: file.name,
+ content_type: file.type,
+ byte_size: file.size,
+ checksum: checksum
+ };
+ this.xhr = new XMLHttpRequest();
+ this.xhr.open("POST", url, true);
+ this.xhr.responseType = "json";
+ this.xhr.setRequestHeader("Content-Type", "application/json");
+ this.xhr.setRequestHeader("Accept", "application/json");
+ this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ this.xhr.setRequestHeader("X-CSRF-Token", getMetaValue("csrf-token"));
+ this.xhr.addEventListener("load", function(event) {
+ return _this.requestDidLoad(event);
+ });
+ this.xhr.addEventListener("error", function(event) {
+ return _this.requestDidError(event);
+ });
+ }
+ createClass(BlobRecord, [ {
+ key: "create",
+ value: function create(callback) {
+ this.callback = callback;
+ this.xhr.send(JSON.stringify({
+ blob: this.attributes
+ }));
+ }
+ }, {
+ key: "requestDidLoad",
+ value: function requestDidLoad(event) {
+ if (this.status >= 200 && this.status < 300) {
+ var response = this.response;
+ var direct_upload = response.direct_upload;
+ delete response.direct_upload;
+ this.attributes = response;
+ this.directUploadData = direct_upload;
+ this.callback(null, this.toJSON());
+ } else {
+ this.requestDidError(event);
+ }
+ }
+ }, {
+ key: "requestDidError",
+ value: function requestDidError(event) {
+ this.callback('Error creating Blob for "' + this.file.name + '". Status: ' + this.status);
+ }
+ }, {
+ key: "toJSON",
+ value: function toJSON() {
+ var result = {};
+ for (var key in this.attributes) {
+ result[key] = this.attributes[key];
+ }
+ return result;
+ }
+ }, {
+ key: "status",
+ get: function get$$1() {
+ return this.xhr.status;
+ }
+ }, {
+ key: "response",
+ get: function get$$1() {
+ var _xhr = this.xhr, responseType = _xhr.responseType, response = _xhr.response;
+ if (responseType == "json") {
+ return response;
+ } else {
+ return JSON.parse(response);
+ }
+ }
+ } ]);
+ return BlobRecord;
+ }();
+ var BlobUpload = function() {
+ function BlobUpload(blob) {
+ var _this = this;
+ classCallCheck(this, BlobUpload);
+ this.blob = blob;
+ this.file = blob.file;
+ var _blob$directUploadDat = blob.directUploadData, url = _blob$directUploadDat.url, headers = _blob$directUploadDat.headers;
+ this.xhr = new XMLHttpRequest();
+ this.xhr.open("PUT", url, true);
+ this.xhr.responseType = "text";
+ for (var key in headers) {
+ this.xhr.setRequestHeader(key, headers[key]);
+ }
+ this.xhr.addEventListener("load", function(event) {
+ return _this.requestDidLoad(event);
+ });
+ this.xhr.addEventListener("error", function(event) {
+ return _this.requestDidError(event);
+ });
+ }
+ createClass(BlobUpload, [ {
+ key: "create",
+ value: function create(callback) {
+ this.callback = callback;
+ this.xhr.send(this.file.slice());
+ }
+ }, {
+ key: "requestDidLoad",
+ value: function requestDidLoad(event) {
+ var _xhr = this.xhr, status = _xhr.status, response = _xhr.response;
+ if (status >= 200 && status < 300) {
+ this.callback(null, response);
+ } else {
+ this.requestDidError(event);
+ }
+ }
+ }, {
+ key: "requestDidError",
+ value: function requestDidError(event) {
+ this.callback('Error storing "' + this.file.name + '". Status: ' + this.xhr.status);
+ }
+ } ]);
+ return BlobUpload;
+ }();
+ var id = 0;
+ var DirectUpload = function() {
+ function DirectUpload(file, url, delegate) {
+ classCallCheck(this, DirectUpload);
+ this.id = ++id;
+ this.file = file;
+ this.url = url;
+ this.delegate = delegate;
+ }
+ createClass(DirectUpload, [ {
+ key: "create",
+ value: function create(callback) {
+ var _this = this;
+ FileChecksum.create(this.file, function(error, checksum) {
+ if (error) {
+ callback(error);
+ return;
+ }
+ var blob = new BlobRecord(_this.file, checksum, _this.url);
+ notify(_this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
+ blob.create(function(error) {
+ if (error) {
+ callback(error);
+ } else {
+ var upload = new BlobUpload(blob);
+ notify(_this.delegate, "directUploadWillStoreFileWithXHR", upload.xhr);
+ upload.create(function(error) {
+ if (error) {
+ callback(error);
+ } else {
+ callback(null, blob.toJSON());
+ }
+ });
+ }
+ });
+ });
+ }
+ } ]);
+ return DirectUpload;
+ }();
+ function notify(object, methodName) {
+ if (object && typeof object[methodName] == "function") {
+ for (var _len = arguments.length, messages = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
+ messages[_key - 2] = arguments[_key];
+ }
+ return object[methodName].apply(object, messages);
+ }
+ }
+ var DirectUploadController = function() {
+ function DirectUploadController(input, file) {
+ classCallCheck(this, DirectUploadController);
+ this.input = input;
+ this.file = file;
+ this.directUpload = new DirectUpload(this.file, this.url, this);
+ this.dispatch("initialize");
+ }
+ createClass(DirectUploadController, [ {
+ key: "start",
+ value: function start(callback) {
+ var _this = this;
+ var hiddenInput = document.createElement("input");
+ hiddenInput.type = "hidden";
+ hiddenInput.name = this.input.name;
+ this.input.insertAdjacentElement("beforebegin", hiddenInput);
+ this.dispatch("start");
+ this.directUpload.create(function(error, attributes) {
+ if (error) {
+ hiddenInput.parentNode.removeChild(hiddenInput);
+ _this.dispatchError(error);
+ } else {
+ hiddenInput.value = attributes.signed_id;
+ }
+ _this.dispatch("end");
+ callback(error);
+ });
+ }
+ }, {
+ key: "uploadRequestDidProgress",
+ value: function uploadRequestDidProgress(event) {
+ var progress = event.loaded / event.total * 100;
+ if (progress) {
+ this.dispatch("progress", {
+ progress: progress
+ });
+ }
+ }
+ }, {
+ key: "dispatch",
+ value: function dispatch(name) {
+ var detail = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ detail.file = this.file;
+ detail.id = this.directUpload.id;
+ return dispatchEvent(this.input, "direct-upload:" + name, {
+ detail: detail
+ });
+ }
+ }, {
+ key: "dispatchError",
+ value: function dispatchError(error) {
+ var event = this.dispatch("error", {
+ error: error
+ });
+ if (!event.defaultPrevented) {
+ alert(error);
+ }
+ }
+ }, {
+ key: "directUploadWillCreateBlobWithXHR",
+ value: function directUploadWillCreateBlobWithXHR(xhr) {
+ this.dispatch("before-blob-request", {
+ xhr: xhr
+ });
+ }
+ }, {
+ key: "directUploadWillStoreFileWithXHR",
+ value: function directUploadWillStoreFileWithXHR(xhr) {
+ var _this2 = this;
+ this.dispatch("before-storage-request", {
+ xhr: xhr
+ });
+ xhr.upload.addEventListener("progress", function(event) {
+ return _this2.uploadRequestDidProgress(event);
+ });
+ }
+ }, {
+ key: "url",
+ get: function get$$1() {
+ return this.input.getAttribute("data-direct-upload-url");
+ }
+ } ]);
+ return DirectUploadController;
+ }();
+ var inputSelector = "input[type=file][data-direct-upload-url]:not([disabled])";
+ var DirectUploadsController = function() {
+ function DirectUploadsController(form) {
+ classCallCheck(this, DirectUploadsController);
+ this.form = form;
+ this.inputs = findElements(form, inputSelector).filter(function(input) {
+ return input.files.length;
+ });
+ }
+ createClass(DirectUploadsController, [ {
+ key: "start",
+ value: function start(callback) {
+ var _this = this;
+ var controllers = this.createDirectUploadControllers();
+ var startNextController = function startNextController() {
+ var controller = controllers.shift();
+ if (controller) {
+ controller.start(function(error) {
+ if (error) {
+ callback(error);
+ _this.dispatch("end");
+ } else {
+ startNextController();
+ }
+ });
+ } else {
+ callback();
+ _this.dispatch("end");
+ }
+ };
+ this.dispatch("start");
+ startNextController();
+ }
+ }, {
+ key: "createDirectUploadControllers",
+ value: function createDirectUploadControllers() {
+ var controllers = [];
+ this.inputs.forEach(function(input) {
+ toArray$1(input.files).forEach(function(file) {
+ var controller = new DirectUploadController(input, file);
+ controllers.push(controller);
+ });
+ });
+ return controllers;
+ }
+ }, {
+ key: "dispatch",
+ value: function dispatch(name) {
+ var detail = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ return dispatchEvent(this.form, "direct-uploads:" + name, {
+ detail: detail
+ });
+ }
+ } ]);
+ return DirectUploadsController;
+ }();
+ var processingAttribute = "data-direct-uploads-processing";
+ var started = false;
+ function start() {
+ if (!started) {
+ started = true;
+ document.addEventListener("submit", didSubmitForm);
+ document.addEventListener("ajax:before", didSubmitRemoteElement);
+ }
+ }
+ function didSubmitForm(event) {
+ handleFormSubmissionEvent(event);
+ }
+ function didSubmitRemoteElement(event) {
+ if (event.target.tagName == "FORM") {
+ handleFormSubmissionEvent(event);
+ }
+ }
+ function handleFormSubmissionEvent(event) {
+ var form = event.target;
+ if (form.hasAttribute(processingAttribute)) {
+ event.preventDefault();
+ return;
+ }
+ var controller = new DirectUploadsController(form);
+ var inputs = controller.inputs;
+ if (inputs.length) {
+ event.preventDefault();
+ form.setAttribute(processingAttribute, "");
+ inputs.forEach(disable);
+ controller.start(function(error) {
+ form.removeAttribute(processingAttribute);
+ if (error) {
+ inputs.forEach(enable);
+ } else {
+ submitForm(form);
+ }
+ });
+ }
+ }
+ function submitForm(form) {
+ var button = findElement(form, "input[type=submit]");
+ if (button) {
+ var _button = button, disabled = _button.disabled;
+ button.disabled = false;
+ button.focus();
+ button.click();
+ button.disabled = disabled;
+ } else {
+ button = document.createElement("input");
+ button.type = "submit";
+ button.style.display = "none";
+ form.appendChild(button);
+ button.click();
+ form.removeChild(button);
+ }
+ }
+ function disable(input) {
+ input.disabled = true;
+ }
+ function enable(input) {
+ input.disabled = false;
+ }
+ function autostart() {
+ if (window.ActiveStorage) {
+ start();
+ }
+ }
+ setTimeout(autostart, 1);
+ exports.start = start;
+ exports.DirectUpload = DirectUpload;
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+});
diff --git a/activestorage/app/controllers/active_storage/base_controller.rb b/activestorage/app/controllers/active_storage/base_controller.rb
new file mode 100644
index 0000000000..59312ac8df
--- /dev/null
+++ b/activestorage/app/controllers/active_storage/base_controller.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+# The base controller for all ActiveStorage controllers.
+class ActiveStorage::BaseController < ActionController::Base
+ protect_from_forgery with: :exception
+
+ before_action do
+ ActiveStorage::Current.host = request.base_url
+ end
+end
diff --git a/activestorage/app/controllers/active_storage/blobs_controller.rb b/activestorage/app/controllers/active_storage/blobs_controller.rb
index 00aa8567c8..92e54c386d 100644
--- a/activestorage/app/controllers/active_storage/blobs_controller.rb
+++ b/activestorage/app/controllers/active_storage/blobs_controller.rb
@@ -4,22 +4,11 @@
# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
# security-through-obscurity factor of the signed blob references, you'll need to implement your own
# authenticated redirection controller.
-class ActiveStorage::BlobsController < ActionController::Base
+class ActiveStorage::BlobsController < ActiveStorage::BaseController
+ include ActiveStorage::SetBlob
+
def show
- if blob = find_signed_blob
- expires_in 5.minutes # service_url defaults to 5 minutes
- redirect_to blob.service_url(disposition: disposition_param)
- else
- head :not_found
- end
+ expires_in ActiveStorage::Blob.service.url_expires_in
+ redirect_to @blob.service_url(disposition: params[:disposition])
end
-
- private
- def find_signed_blob
- ActiveStorage::Blob.find_signed(params[:signed_id])
- end
-
- def disposition_param
- params[:disposition].presence_in(%w( inline attachment )) || "inline"
- end
end
diff --git a/activestorage/app/controllers/active_storage/direct_uploads_controller.rb b/activestorage/app/controllers/active_storage/direct_uploads_controller.rb
index 205d173648..78b43fc94c 100644
--- a/activestorage/app/controllers/active_storage/direct_uploads_controller.rb
+++ b/activestorage/app/controllers/active_storage/direct_uploads_controller.rb
@@ -3,7 +3,7 @@
# Creates a new blob on the server side in anticipation of a direct-to-service upload from the client side.
# When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference
# the blob that was created up front.
-class ActiveStorage::DirectUploadsController < ActionController::Base
+class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
def create
blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args)
render json: direct_upload_json(blob)
@@ -15,7 +15,7 @@ class ActiveStorage::DirectUploadsController < ActionController::Base
end
def direct_upload_json(blob)
- blob.as_json(methods: :signed_id).merge(direct_upload: {
+ blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
url: blob.service_url_for_direct_upload,
headers: blob.service_headers_for_direct_upload
})
diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb
index 41e6d61bff..7bc5eb3fdb 100644
--- a/activestorage/app/controllers/active_storage/disk_controller.rb
+++ b/activestorage/app/controllers/active_storage/disk_controller.rb
@@ -4,11 +4,13 @@
# This means using expiring, signed URLs that are meant for immediate access, not permanent linking.
# Always go through the BlobsController, or your own authenticated controller, rather than directly
# to the service url.
-class ActiveStorage::DiskController < ActionController::Base
+class ActiveStorage::DiskController < ActiveStorage::BaseController
+ skip_forgery_protection
+
def show
if key = decode_verified_key
send_data disk_service.download(key),
- disposition: disposition_param, content_type: params[:content_type]
+ disposition: params[:disposition], content_type: params[:content_type]
else
head :not_found
end
@@ -38,10 +40,6 @@ class ActiveStorage::DiskController < ActionController::Base
ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
end
- def disposition_param
- params[:disposition].presence || "inline"
- end
-
def decode_verified_token
ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
diff --git a/activestorage/app/controllers/active_storage/representations_controller.rb b/activestorage/app/controllers/active_storage/representations_controller.rb
new file mode 100644
index 0000000000..ce9286db7d
--- /dev/null
+++ b/activestorage/app/controllers/active_storage/representations_controller.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Take a signed permanent reference for a blob representation and turn it into an expiring service URL for download.
+# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
+# security-through-obscurity factor of the signed blob and variation reference, you'll need to implement your own
+# authenticated redirection controller.
+class ActiveStorage::RepresentationsController < ActiveStorage::BaseController
+ include ActiveStorage::SetBlob
+
+ def show
+ expires_in ActiveStorage::Blob.service.url_expires_in
+ redirect_to @blob.representation(params[:variation_key]).processed.service_url(disposition: params[:disposition])
+ end
+end
diff --git a/activestorage/app/controllers/active_storage/variants_controller.rb b/activestorage/app/controllers/active_storage/variants_controller.rb
deleted file mode 100644
index 02e3010626..0000000000
--- a/activestorage/app/controllers/active_storage/variants_controller.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-# Take a signed permanent reference for a variant and turn it into an expiring service URL for download.
-# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
-# security-through-obscurity factor of the signed blob and variation reference, you'll need to implement your own
-# authenticated redirection controller.
-class ActiveStorage::VariantsController < ActionController::Base
- def show
- if blob = find_signed_blob
- expires_in 5.minutes # service_url defaults to 5 minutes
- redirect_to ActiveStorage::Variant.new(blob, decoded_variation).processed.service_url(disposition: disposition_param)
- else
- head :not_found
- end
- end
-
- private
- def find_signed_blob
- ActiveStorage::Blob.find_signed(params[:signed_blob_id])
- end
-
- def decoded_variation
- ActiveStorage::Variation.decode(params[:variation_key])
- end
-
- def disposition_param
- params[:disposition].presence_in(%w( inline attachment )) || "inline"
- end
-end
diff --git a/activestorage/app/controllers/concerns/active_storage/set_blob.rb b/activestorage/app/controllers/concerns/active_storage/set_blob.rb
new file mode 100644
index 0000000000..f072954d78
--- /dev/null
+++ b/activestorage/app/controllers/concerns/active_storage/set_blob.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module ActiveStorage::SetBlob #:nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :set_blob
+ end
+
+ private
+ def set_blob
+ @blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id])
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
+ head :not_found
+ end
+end
diff --git a/activestorage/app/javascript/activestorage/blob_record.js b/activestorage/app/javascript/activestorage/blob_record.js
index 3c6e6b6ba1..ff847892b2 100644
--- a/activestorage/app/javascript/activestorage/blob_record.js
+++ b/activestorage/app/javascript/activestorage/blob_record.js
@@ -22,14 +22,28 @@ export class BlobRecord {
this.xhr.addEventListener("error", event => this.requestDidError(event))
}
+ get status() {
+ return this.xhr.status
+ }
+
+ get response() {
+ const { responseType, response } = this.xhr
+ if (responseType == "json") {
+ return response
+ } else {
+ // Shim for IE 11: https://connect.microsoft.com/IE/feedback/details/794808
+ return JSON.parse(response)
+ }
+ }
+
create(callback) {
this.callback = callback
this.xhr.send(JSON.stringify({ blob: this.attributes }))
}
requestDidLoad(event) {
- const { status, response } = this.xhr
- if (status >= 200 && status < 300) {
+ if (this.status >= 200 && this.status < 300) {
+ const { response } = this
const { direct_upload } = response
delete response.direct_upload
this.attributes = response
@@ -41,7 +55,7 @@ export class BlobRecord {
}
requestDidError(event) {
- this.callback(`Error creating Blob for "${this.file.name}". Status: ${this.xhr.status}`)
+ this.callback(`Error creating Blob for "${this.file.name}". Status: ${this.status}`)
}
toJSON() {
diff --git a/activestorage/app/javascript/activestorage/blob_upload.js b/activestorage/app/javascript/activestorage/blob_upload.js
index 180a7415e7..277cc8ff8e 100644
--- a/activestorage/app/javascript/activestorage/blob_upload.js
+++ b/activestorage/app/javascript/activestorage/blob_upload.js
@@ -17,7 +17,7 @@ export class BlobUpload {
create(callback) {
this.callback = callback
- this.xhr.send(this.file)
+ this.xhr.send(this.file.slice())
}
requestDidLoad(event) {
diff --git a/activestorage/app/javascript/activestorage/direct_upload.js b/activestorage/app/javascript/activestorage/direct_upload.js
index 7085e0a4ab..c2eedf289b 100644
--- a/activestorage/app/javascript/activestorage/direct_upload.js
+++ b/activestorage/app/javascript/activestorage/direct_upload.js
@@ -14,8 +14,14 @@ export class DirectUpload {
create(callback) {
FileChecksum.create(this.file, (error, checksum) => {
+ if (error) {
+ callback(error)
+ return
+ }
+
const blob = new BlobRecord(this.file, checksum, this.url)
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
+
blob.create(error => {
if (error) {
callback(error)
diff --git a/activestorage/app/javascript/activestorage/helpers.js b/activestorage/app/javascript/activestorage/helpers.js
index 52fec8f6f1..7e83c447e7 100644
--- a/activestorage/app/javascript/activestorage/helpers.js
+++ b/activestorage/app/javascript/activestorage/helpers.js
@@ -23,11 +23,20 @@ export function findElement(root, selector) {
}
export function dispatchEvent(element, type, eventInit = {}) {
+ const { disabled } = element
const { bubbles, cancelable, detail } = eventInit
const event = document.createEvent("Event")
+
event.initEvent(type, bubbles || true, cancelable || true)
event.detail = detail || {}
- element.dispatchEvent(event)
+
+ try {
+ element.disabled = false
+ element.dispatchEvent(event)
+ } finally {
+ element.disabled = disabled
+ }
+
return event
}
diff --git a/activestorage/app/javascript/activestorage/ujs.js b/activestorage/app/javascript/activestorage/ujs.js
index 1dda02936f..08c535470d 100644
--- a/activestorage/app/javascript/activestorage/ujs.js
+++ b/activestorage/app/javascript/activestorage/ujs.js
@@ -59,7 +59,7 @@ function submitForm(form) {
} else {
button = document.createElement("input")
button.type = "submit"
- button.style = "display:none"
+ button.style.display = "none"
form.appendChild(button)
button.click()
form.removeChild(button)
diff --git a/activestorage/app/jobs/active_storage/analyze_job.rb b/activestorage/app/jobs/active_storage/analyze_job.rb
new file mode 100644
index 0000000000..2a952f9f74
--- /dev/null
+++ b/activestorage/app/jobs/active_storage/analyze_job.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later.
+class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob
+ def perform(blob)
+ blob.analyze
+ end
+end
diff --git a/activestorage/app/jobs/active_storage/base_job.rb b/activestorage/app/jobs/active_storage/base_job.rb
new file mode 100644
index 0000000000..6caab42a2d
--- /dev/null
+++ b/activestorage/app/jobs/active_storage/base_job.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ActiveStorage::BaseJob < ActiveJob::Base
+ queue_as { ActiveStorage.queue }
+end
diff --git a/activestorage/app/jobs/active_storage/purge_job.rb b/activestorage/app/jobs/active_storage/purge_job.rb
index 990ab27c9f..98874d2250 100644
--- a/activestorage/app/jobs/active_storage/purge_job.rb
+++ b/activestorage/app/jobs/active_storage/purge_job.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-# Provides delayed purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later.
-class ActiveStorage::PurgeJob < ActiveJob::Base
+# Provides asynchronous purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later.
+class ActiveStorage::PurgeJob < ActiveStorage::BaseJob
# FIXME: Limit this to a custom ActiveStorage error
retry_on StandardError
diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb
index 29226e8ee9..c59877a9a5 100644
--- a/activestorage/app/models/active_storage/attachment.rb
+++ b/activestorage/app/models/active_storage/attachment.rb
@@ -14,6 +14,8 @@ class ActiveStorage::Attachment < ActiveRecord::Base
delegate_missing_to :blob
+ after_create_commit :analyze_blob_later, :identify_blob
+
# Synchronously purges the blob (deletes it from the configured service) and destroys the attachment.
def purge
blob.purge
@@ -25,4 +27,13 @@ class ActiveStorage::Attachment < ActiveRecord::Base
blob.purge_later
destroy
end
+
+ private
+ def identify_blob
+ blob.identify
+ end
+
+ def analyze_blob_later
+ blob.analyze_later unless blob.analyzed?
+ end
end
diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb
index e6cf08ce83..0cd4ad8128 100644
--- a/activestorage/app/models/active_storage/blob.rb
+++ b/activestorage/app/models/active_storage/blob.rb
@@ -14,13 +14,25 @@
# update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
# If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
class ActiveStorage::Blob < ActiveRecord::Base
+ require_dependency "active_storage/blob/analyzable"
+ require_dependency "active_storage/blob/identifiable"
+ require_dependency "active_storage/blob/representable"
+
+ include Analyzable
+ include Identifiable
+ include Representable
+
self.table_name = "active_storage_blobs"
has_secure_token :key
- store :metadata, coder: JSON
+ store :metadata, accessors: [ :analyzed, :identified ], coder: ActiveRecord::Coders::JSON
class_attribute :service
+ has_many :attachments
+
+ scope :unattached, -> { left_joins(:attachments).where(ActiveStorage::Attachment.table_name => { blob_id: nil }) }
+
class << self
# You can used the signed ID of a blob to refer to it on the client side without fear of tampering.
# This is particularly helpful for direct uploads where the client-side needs to refer to the blob
@@ -59,7 +71,6 @@ class ActiveStorage::Blob < ActiveRecord::Base
end
end
-
# Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
# It uses the framework-wide verifier on <tt>ActiveStorage.verifier</tt>, but with a dedicated purpose.
def signed_id
@@ -101,38 +112,21 @@ class ActiveStorage::Blob < ActiveRecord::Base
content_type.start_with?("text")
end
- # Returns an ActiveStorage::Variant instance with the set of +transformations+
- # passed in. This is only relevant for image files, and it allows any image to
- # be transformed for size, colors, and the like. Example:
- #
- # avatar.variant(resize: "100x100").processed.service_url
- #
- # This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
- # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
- #
- # Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a
- # specific variant that can be created by a controller on-demand. Like so:
- #
- # <%= image_tag url_for(Current.user.avatar.variant(resize: "100x100")) %>
- #
- # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController
- # can then produce on-demand.
- def variant(transformations)
- ActiveStorage::Variant.new(self, ActiveStorage::Variation.new(transformations))
- end
-
# Returns the URL of the blob on the service. This URL is intended to be short-lived for security and not used directly
# with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
# Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
# it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
- def service_url(expires_in: 5.minutes, disposition: :inline)
- service.url key, expires_in: expires_in, disposition: "#{disposition}; #{filename.parameters}", filename: filename, content_type: content_type
+ def service_url(expires_in: service.url_expires_in, disposition: :inline, filename: nil, **options)
+ filename = ActiveStorage::Filename.wrap(filename || self.filename)
+
+ service.url key, expires_in: expires_in, filename: filename, content_type: content_type,
+ disposition: forcibly_serve_as_binary? ? :attachment : disposition, **options
end
# Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be
# short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
- def service_url_for_direct_upload(expires_in: 5.minutes)
+ def service_url_for_direct_upload(expires_in: service.url_expires_in)
service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum
end
@@ -141,6 +135,7 @@ class ActiveStorage::Blob < ActiveRecord::Base
service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum
end
+
# Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be
# using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob,
# you should instead simply create a new blob based on the old one.
@@ -152,8 +147,10 @@ class ActiveStorage::Blob < ActiveRecord::Base
# Normally, you do not have to call this method directly at all. Use the factory class methods of +build_after_upload+
# and +create_after_upload!+.
def upload(io)
- self.checksum = compute_checksum_in_chunks(io)
- self.byte_size = io.size
+ self.checksum = compute_checksum_in_chunks(io)
+ self.content_type = extract_content_type(io)
+ self.byte_size = io.size
+ self.identified = true
service.upload(key, io, checksum: checksum)
end
@@ -169,7 +166,8 @@ class ActiveStorage::Blob < ActiveRecord::Base
# deleted as well or you will essentially have a dead reference. It's recommended to use the +#purge+ and +#purge_later+
# methods in most circumstances.
def delete
- service.delete key
+ service.delete(key)
+ service.delete_prefixed("variants/#{key}/") if image?
end
# Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted
@@ -196,4 +194,14 @@ class ActiveStorage::Blob < ActiveRecord::Base
io.rewind
end.base64digest
end
+
+ def extract_content_type(io)
+ Marcel::MimeType.for io, name: filename.to_s, declared_type: content_type
+ end
+
+ def forcibly_serve_as_binary?
+ ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
+ end
+
+ ActiveSupport.run_load_hooks(:active_storage_blob, self)
end
diff --git a/activestorage/app/models/active_storage/blob/analyzable.rb b/activestorage/app/models/active_storage/blob/analyzable.rb
new file mode 100644
index 0000000000..5bda6e6d73
--- /dev/null
+++ b/activestorage/app/models/active_storage/blob/analyzable.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require "active_storage/analyzer/null_analyzer"
+
+module ActiveStorage::Blob::Analyzable
+ # Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes
+ # with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and
+ # ActiveStorage::Analyzer::VideoAnalyzer for information about the specific attributes they extract and the third-party
+ # libraries they require.
+ #
+ # To choose the analyzer for a blob, Active Storage calls +accept?+ on each registered analyzer in order. It uses the
+ # first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no
+ # metadata is extracted from it.
+ #
+ # In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
+ # in an initializer:
+ #
+ # # Add a custom analyzer for Microsoft Office documents:
+ # Rails.application.config.active_storage.analyzers.append DOCXAnalyzer
+ #
+ # # Remove the built-in video analyzer:
+ # Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
+ #
+ # Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead.
+ #
+ # You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously
+ # analyzed via #analyze_later when they're attached for the first time.
+ def analyze
+ update! metadata: metadata.merge(extract_metadata_via_analyzer)
+ end
+
+ # Enqueues an ActiveStorage::AnalyzeJob which calls #analyze.
+ #
+ # This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob
+ # again (e.g. if you add a new analyzer or modify an existing one).
+ def analyze_later
+ ActiveStorage::AnalyzeJob.perform_later(self)
+ end
+
+ # Returns true if the blob has been analyzed.
+ def analyzed?
+ analyzed
+ end
+
+ private
+ def extract_metadata_via_analyzer
+ analyzer.metadata.merge(analyzed: true)
+ end
+
+ def analyzer
+ analyzer_class.new(self)
+ end
+
+ def analyzer_class
+ ActiveStorage.analyzers.detect { |klass| klass.accept?(self) } || ActiveStorage::Analyzer::NullAnalyzer
+ end
+end
diff --git a/activestorage/app/models/active_storage/blob/identifiable.rb b/activestorage/app/models/active_storage/blob/identifiable.rb
new file mode 100644
index 0000000000..049e45dc3e
--- /dev/null
+++ b/activestorage/app/models/active_storage/blob/identifiable.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module ActiveStorage::Blob::Identifiable
+ def identify
+ update! content_type: identify_content_type, identified: true unless identified?
+ end
+
+ def identified?
+ identified
+ end
+
+ private
+ def identify_content_type
+ Marcel::MimeType.for download_identifiable_chunk, name: filename.to_s, declared_type: content_type
+ end
+
+ def download_identifiable_chunk
+ service.download_chunk key, 0...4.kilobytes
+ end
+end
diff --git a/activestorage/app/models/active_storage/blob/representable.rb b/activestorage/app/models/active_storage/blob/representable.rb
new file mode 100644
index 0000000000..03d5511481
--- /dev/null
+++ b/activestorage/app/models/active_storage/blob/representable.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module ActiveStorage::Blob::Representable
+ extend ActiveSupport::Concern
+
+ included do
+ has_one_attached :preview_image
+ end
+
+ # Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image
+ # files, and it allows any image to be transformed for size, colors, and the like. Example:
+ #
+ # avatar.variant(resize_to_fit: [100, 100]).processed.service_url
+ #
+ # This will create and process a variant of the avatar blob that's constrained to a height and width of 100px.
+ # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
+ #
+ # Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a
+ # specific variant that can be created by a controller on-demand. Like so:
+ #
+ # <%= image_tag Current.user.avatar.variant(resize_to_fit: [100, 100]) %>
+ #
+ # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
+ # can then produce on-demand.
+ #
+ # Raises ActiveStorage::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is
+ # variable, call ActiveStorage::Blob#variable?.
+ def variant(transformations)
+ if variable?
+ ActiveStorage::Variant.new(self, transformations)
+ else
+ raise ActiveStorage::InvariableError
+ end
+ end
+
+ # Returns true if ImageMagick can transform the blob (its content type is in +ActiveStorage.variable_content_types+).
+ def variable?
+ ActiveStorage.variable_content_types.include?(content_type)
+ end
+
+
+ # Returns an ActiveStorage::Preview instance with the set of +transformations+ provided. A preview is an image generated
+ # from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer
+ # extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document.
+ #
+ # blob.preview(resize_to_fit: [100, 100]).processed.service_url
+ #
+ # Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand.
+ # Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s
+ # how to use the built-in version:
+ #
+ # <%= image_tag video.preview(resize_to_fit: [100, 100]) %>
+ #
+ # This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine
+ # whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?.
+ def preview(transformations)
+ if previewable?
+ ActiveStorage::Preview.new(self, transformations)
+ else
+ raise ActiveStorage::UnpreviewableError
+ end
+ end
+
+ # Returns true if any registered previewer accepts the blob. By default, this will return true for videos and PDF documents.
+ def previewable?
+ ActiveStorage.previewers.any? { |klass| klass.accept?(self) }
+ end
+
+
+ # Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob.
+ #
+ # blob.representation(resize_to_fit: [100, 100]).processed.service_url
+ #
+ # Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call
+ # ActiveStorage::Blob#representable? to determine whether a blob is representable.
+ #
+ # See ActiveStorage::Blob#preview and ActiveStorage::Blob#variant for more information.
+ def representation(transformations)
+ case
+ when previewable?
+ preview transformations
+ when variable?
+ variant transformations
+ else
+ raise ActiveStorage::UnrepresentableError
+ end
+ end
+
+ # Returns true if the blob is variable or previewable.
+ def representable?
+ variable? || previewable?
+ end
+end
diff --git a/activestorage/app/models/active_storage/current.rb b/activestorage/app/models/active_storage/current.rb
new file mode 100644
index 0000000000..7e431d8462
--- /dev/null
+++ b/activestorage/app/models/active_storage/current.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ActiveStorage::Current < ActiveSupport::CurrentAttributes #:nodoc:
+ attribute :host
+end
diff --git a/activestorage/app/models/active_storage/filename.rb b/activestorage/app/models/active_storage/filename.rb
index dead6b6d33..bebb5e61b3 100644
--- a/activestorage/app/models/active_storage/filename.rb
+++ b/activestorage/app/models/active_storage/filename.rb
@@ -1,31 +1,49 @@
# frozen_string_literal: true
-# Encapsulates a string representing a filename to provide convenience access to parts of it and a sanitized version.
-# This is what's returned by ActiveStorage::Blob#filename. A Filename instance is comparable so it can be used for sorting.
+# Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
+# A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
class ActiveStorage::Filename
+ require_dependency "active_storage/filename/parameters"
+
include Comparable
+ class << self
+ # Returns a Filename instance based on the given filename. If the filename is a Filename, it is
+ # returned unmodified. If it is a String, it is passed to ActiveStorage::Filename.new.
+ def wrap(filename)
+ filename.kind_of?(self) ? filename : new(filename)
+ end
+ end
+
def initialize(filename)
@filename = filename
end
- # Returns the basename of the filename.
+ # Returns the part of the filename preceding any extension.
#
# ActiveStorage::Filename.new("racecar.jpg").base # => "racecar"
+ # ActiveStorage::Filename.new("racecar").base # => "racecar"
+ # ActiveStorage::Filename.new(".gitignore").base # => ".gitignore"
def base
File.basename @filename, extension_with_delimiter
end
- # Returns the extension with delimiter of the filename.
+ # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at the
+ # beginning) with the dot that precedes it. If the filename has no extension, an empty string is returned.
#
# ActiveStorage::Filename.new("racecar.jpg").extension_with_delimiter # => ".jpg"
+ # ActiveStorage::Filename.new("racecar").extension_with_delimiter # => ""
+ # ActiveStorage::Filename.new(".gitignore").extension_with_delimiter # => ""
def extension_with_delimiter
File.extname @filename
end
- # Returns the extension without delimiter of the filename.
+ # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at
+ # the beginning). If the filename has no extension, an empty string is returned.
#
# ActiveStorage::Filename.new("racecar.jpg").extension_without_delimiter # => "jpg"
+ # ActiveStorage::Filename.new("racecar").extension_without_delimiter # => ""
+ # ActiveStorage::Filename.new(".gitignore").extension_without_delimiter # => ""
def extension_without_delimiter
extension_with_delimiter.from(1).to_s
end
@@ -37,12 +55,12 @@ class ActiveStorage::Filename
# ActiveStorage::Filename.new("foo:bar.jpg").sanitized # => "foo-bar.jpg"
# ActiveStorage::Filename.new("foo/bar.jpg").sanitized # => "foo-bar.jpg"
#
- # ...and any other character unsafe for URLs or storage is converted or stripped.
+ # Characters considered unsafe for storage (e.g. \, $, and the RTL override character) are replaced with a dash.
def sanitized
@filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
end
- def parameters
+ def parameters #:nodoc:
Parameters.new self
end
diff --git a/activestorage/app/models/active_storage/filename/parameters.rb b/activestorage/app/models/active_storage/filename/parameters.rb
index 58ce198d38..fb9ea10e49 100644
--- a/activestorage/app/models/active_storage/filename/parameters.rb
+++ b/activestorage/app/models/active_storage/filename/parameters.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class ActiveStorage::Filename::Parameters
+class ActiveStorage::Filename::Parameters #:nodoc:
attr_reader :filename
def initialize(filename)
diff --git a/activestorage/app/models/active_storage/preview.rb b/activestorage/app/models/active_storage/preview.rb
new file mode 100644
index 0000000000..de58763399
--- /dev/null
+++ b/activestorage/app/models/active_storage/preview.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+# Some non-image blobs can be previewed: that is, they can be presented as images. A video blob can be previewed by
+# extracting its first frame, and a PDF blob can be previewed by extracting its first page.
+#
+# A previewer extracts a preview image from a blob. Active Storage provides previewers for videos and PDFs:
+# ActiveStorage::Previewer::VideoPreviewer and ActiveStorage::Previewer::PDFPreviewer. Build custom previewers by
+# subclassing ActiveStorage::Previewer and implementing the requisite methods. Consult the ActiveStorage::Previewer
+# documentation for more details on what's required of previewers.
+#
+# To choose the previewer for a blob, Active Storage calls +accept?+ on each registered previewer in order. It uses the
+# first previewer for which +accept?+ returns true when given the blob. In a Rails application, add or remove previewers
+# by manipulating +Rails.application.config.active_storage.previewers+ in an initializer:
+#
+# Rails.application.config.active_storage.previewers
+# # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
+#
+# # Add a custom previewer for Microsoft Office documents:
+# Rails.application.config.active_storage.previewers << DOCXPreviewer
+# # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ]
+#
+# Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
+#
+# The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires
+# {ffmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
+# and the other requires {mupdf}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or mupdf.
+#
+# These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
+# install and use third-party software, make sure you understand the licensing implications of doing so.
+class ActiveStorage::Preview
+ class UnprocessedError < StandardError; end
+
+ attr_reader :blob, :variation
+
+ def initialize(blob, variation_or_variation_key)
+ @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
+ end
+
+ # Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience:
+ #
+ # blob.preview(resize_to_fit: [100, 100]).processed.service_url
+ #
+ # Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview
+ # image is stored with the blob, it is only generated once.
+ def processed
+ process unless processed?
+ self
+ end
+
+ # Returns the blob's attached preview image.
+ def image
+ blob.preview_image
+ end
+
+ # Returns the URL of the preview's variant on the service. Raises ActiveStorage::Preview::UnprocessedError if the
+ # preview has not been processed yet.
+ #
+ # This method synchronously processes a variant of the preview image, so do not call it in views. Instead, generate
+ # a stable URL that redirects to the short-lived URL returned by this method.
+ def service_url(**options)
+ if processed?
+ variant.service_url(options)
+ else
+ raise UnprocessedError
+ end
+ end
+
+ private
+ def processed?
+ image.attached?
+ end
+
+ def process
+ previewer.preview { |attachable| image.attach(attachable) }
+ end
+
+ def variant
+ ActiveStorage::Variant.new(image, variation).processed
+ end
+
+
+ def previewer
+ previewer_class.new(blob)
+ end
+
+ def previewer_class
+ ActiveStorage.previewers.detect { |klass| klass.accept?(blob) }
+ end
+end
diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb
index 02bf32b352..b782489a92 100644
--- a/activestorage/app/models/active_storage/variant.rb
+++ b/activestorage/app/models/active_storage/variant.rb
@@ -1,45 +1,67 @@
# frozen_string_literal: true
+require "active_storage/downloading"
+
# Image blobs can have variants that are the result of a set of transformations applied to the original.
# These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
# original.
#
-# Variants rely on {MiniMagick}[https://github.com/minimagick/minimagick] for the actual transformations
-# of the file, so you must add <tt>gem "mini_magick"</tt> to your Gemfile if you wish to use variants.
+# Variants rely on {ImageProcessing}[https://github.com/janko-m/image_processing] gem for the actual transformations
+# of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
+# default, images will be processed with {ImageMagick}[http://imagemagick.org] using the
+# {MiniMagick}[https://github.com/minimagick/minimagick] gem, but you can also switch to the
+# {libvips}[http://jcupitt.github.io/libvips/] processor operated by the {ruby-vips}[https://github.com/jcupitt/ruby-vips]
+# gem).
+#
+# Rails.application.config.active_storage.variant_processor
+# # => :mini_magick
+#
+# Rails.application.config.active_storage.variant_processor = :vips
+# # => :vips
#
-# Note that to create a variant it's necessary to download the entire blob file from the service and load it
-# into memory. The larger the image, the more memory is used. Because of this process, you also want to be
-# considerate about when the variant is actually processed. You shouldn't be processing variants inline in a
-# template, for example. Delay the processing to an on-demand controller, like the one provided in
-# ActiveStorage::VariantsController.
+# Note that to create a variant it's necessary to download the entire blob file from the service. Because of this process,
+# you also want to be considerate about when the variant is actually processed. You shouldn't be processing variants inline
+# in a template, for example. Delay the processing to an on-demand controller, like the one provided in
+# ActiveStorage::RepresentationsController.
#
# To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
# by Active Storage like so:
#
-# <%= image_tag url_for(Current.user.avatar.variant(resize: "100x100")) %>
+# <%= image_tag Current.user.avatar.variant(resize_to_fit: [100, 100]) %>
#
-# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController
+# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
# can then produce on-demand.
#
# When you do want to actually produce the variant needed, call +processed+. This will check that the variant
# has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
# the transformations, upload the variant to the service, and return itself again. Example:
#
-# avatar.variant(resize: "100x100").processed.service_url
+# avatar.variant(resize_to_fit: [100, 100]).processed.service_url
#
# This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
#
-# A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php. You can
-# combine as many as you like freely:
+# You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the
+# ImageProcessing gem (such as +resize_to_fit+):
+#
+# avatar.variant(resize_to_fit: [800, 800], monochrome: true, flip: "-90")
+#
+# Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
#
-# avatar.variant(resize: "100x100", monochrome: true, flip: "-90")
+# * {ImageProcessing::MiniMagick}[https://github.com/janko-m/image_processing/blob/master/doc/minimagick.md#methods]
+# * {ImageMagick reference}[https://www.imagemagick.org/script/mogrify.php]
+# * {ImageProcessing::Vips}[https://github.com/janko-m/image_processing/blob/master/doc/vips.md#methods]
+# * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image]
class ActiveStorage::Variant
+ include ActiveStorage::Downloading
+
+ WEB_IMAGE_CONTENT_TYPES = %w( image/png image/jpeg image/jpg image/gif )
+
attr_reader :blob, :variation
delegate :service, to: :blob
- def initialize(blob, variation)
- @blob, @variation = blob, variation
+ def initialize(blob, variation_or_variation_key)
+ @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
end
# Returns the variant instance itself after it's been processed or an existing processing has been found on the service.
@@ -50,7 +72,7 @@ class ActiveStorage::Variant
# Returns a combination key of the blob and the variation that together identifies a specific variant.
def key
- "variants/#{blob.key}/#{variation.key}"
+ "variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}"
end
# Returns the URL of the variant on the service. This URL is intended to be short-lived for security and not used directly
@@ -59,12 +81,16 @@ class ActiveStorage::Variant
# it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
#
# Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
- # for a variant that points to the ActiveStorage::VariantsController, which in turn will use this +service_call+ method
+ # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
# for its redirection.
- def service_url(expires_in: 5.minutes, disposition: :inline)
- service.url key, expires_in: expires_in, disposition: disposition, filename: blob.filename, content_type: blob.content_type
+ def service_url(expires_in: service.url_expires_in, disposition: :inline)
+ service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
end
+ # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
+ def image
+ self
+ end
private
def processed?
@@ -72,11 +98,38 @@ class ActiveStorage::Variant
end
def process
- service.upload key, transform(service.download(blob.key))
+ download_blob_to_tempfile do |image|
+ transform image do |output|
+ upload output
+ end
+ end
+ end
+
+
+ def filename
+ if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
+ blob.filename
+ else
+ ActiveStorage::Filename.new("#{blob.filename.base}.png")
+ end
+ end
+
+ def content_type
+ blob.content_type.presence_in(WEB_IMAGE_CONTENT_TYPES) || "image/png"
+ end
+
+ def transform(image)
+ format = "png" unless WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
+ result = variation.transform(image, format: format)
+
+ begin
+ yield result
+ ensure
+ result.close!
+ end
end
- def transform(io)
- require "mini_magick"
- File.open MiniMagick::Image.read(io).tap { |image| variation.transform(image) }.path
+ def upload(file)
+ service.upload(key, file)
end
end
diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb
index cf04a879eb..42f00beb82 100644
--- a/activestorage/app/models/active_storage/variation.rb
+++ b/activestorage/app/models/active_storage/variation.rb
@@ -6,14 +6,28 @@
# In case you do need to use this directly, it's instantiated using a hash of transformations where
# the key is the command and the value is the arguments. Example:
#
-# ActiveStorage::Variation.new(resize: "100x100", monochrome: true, trim: true, rotate: "-90")
+# ActiveStorage::Variation.new(resize_to_fit: [100, 100], monochrome: true, trim: true, rotate: "-90")
#
-# A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php.
+# The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
class ActiveStorage::Variation
attr_reader :transformations
class << self
- # Returns a variation instance with the transformations that were encoded by +encode+.
+ # Returns a Variation instance based on the given variator. If the variator is a Variation, it is
+ # returned unmodified. If it is a String, it is passed to ActiveStorage::Variation.decode. Otherwise,
+ # it is assumed to be a transformations Hash and is passed directly to the constructor.
+ def wrap(variator)
+ case variator
+ when self
+ variator
+ when String
+ decode variator
+ else
+ new variator
+ end
+ end
+
+ # Returns a Variation instance with the transformations that were encoded by +encode+.
def decode(key)
new ActiveStorage.verifier.verify(key, purpose: :variation)
end
@@ -29,14 +43,16 @@ class ActiveStorage::Variation
@transformations = transformations
end
- # Accepts an open MiniMagick image instance, like what's returned by <tt>MiniMagick::Image.read(io)</tt>,
- # and performs the +transformations+ against it. The transformed image instance is then returned.
- def transform(image)
- transformations.each do |(method, argument)|
- if eligible_argument?(argument)
- image.public_send(method, argument)
+ # Accepts a File object, performs the +transformations+ against it, and
+ # saves the transformed image into a temporary file. If +format+ is specified
+ # it will be the format of the result image, otherwise the result image
+ # retains the source format.
+ def transform(file, format: nil)
+ ActiveSupport::Notifications.instrument("transform.active_storage") do
+ if processor
+ image_processing_transform(file, format)
else
- image.public_send(method)
+ mini_magick_transform(file, format)
end
end
end
@@ -47,6 +63,66 @@ class ActiveStorage::Variation
end
private
+ # Applies image transformations using the ImageProcessing gem.
+ def image_processing_transform(file, format)
+ operations = transformations.inject([]) do |list, (name, argument)|
+ if name.to_s == "combine_options"
+ ActiveSupport::Deprecation.warn("The ImageProcessing ActiveStorage variant backend doesn't need :combine_options, as it already generates a single MiniMagick command. In Rails 6.1 :combine_options will not be supported anymore.")
+ list.concat argument.to_a
+ else
+ list << [name, argument]
+ end
+ end
+
+ processor
+ .source(file)
+ .loader(page: 0)
+ .convert(format)
+ .apply(operations)
+ .call
+ end
+
+ # Applies image transformations using the MiniMagick gem.
+ def mini_magick_transform(file, format)
+ image = MiniMagick::Image.new(file.path, file)
+
+ transformations.each do |name, argument_or_subtransformations|
+ image.mogrify do |command|
+ if name.to_s == "combine_options"
+ argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument|
+ pass_transform_argument(command, subtransformation_name, subtransformation_argument)
+ end
+ else
+ pass_transform_argument(command, name, argument_or_subtransformations)
+ end
+ end
+ end
+
+ image.format(format) if format
+
+ image.tempfile.tap(&:open)
+ end
+
+ # Returns the ImageProcessing processor class specified by `ActiveStorage.variant_processor`.
+ def processor
+ begin
+ require "image_processing"
+ rescue LoadError
+ ActiveSupport::Deprecation.warn("Using mini_magick gem directly is deprecated and will be removed in Rails 6.1. Please add `gem 'image_processing', '~> 1.2'` to your Gemfile.")
+ return nil
+ end
+
+ ImageProcessing.const_get(ActiveStorage.variant_processor.to_s.camelize) if ActiveStorage.variant_processor
+ end
+
+ def pass_transform_argument(command, method, argument)
+ if eligible_argument?(argument)
+ command.public_send(method, argument)
+ else
+ command.public_send(method)
+ end
+ end
+
def eligible_argument?(argument)
argument.present? && argument != true
end
diff --git a/activestorage/config/routes.rb b/activestorage/config/routes.rb
index c3194887be..20d19f334a 100644
--- a/activestorage/config/routes.rb
+++ b/activestorage/config/routes.rb
@@ -1,30 +1,31 @@
# frozen_string_literal: true
Rails.application.routes.draw do
- get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob, internal: true
+ get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
- direct :rails_blob do |blob|
- route_for(:rails_service_blob, blob.signed_id, blob.filename)
+ direct :rails_blob do |blob, options|
+ route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
end
- resolve("ActiveStorage::Blob") { |blob| route_for(:rails_blob, blob) }
- resolve("ActiveStorage::Attachment") { |attachment| route_for(:rails_blob, attachment.blob) }
+ resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
+ resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
- get "/rails/active_storage/variants/:signed_blob_id/:variation_key/*filename" => "active_storage/variants#show", as: :rails_blob_variation, internal: true
+ get "/rails/active_storage/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
- direct :rails_variant do |variant|
- signed_blob_id = variant.blob.signed_id
- variation_key = variant.variation.key
- filename = variant.blob.filename
+ direct :rails_representation do |representation, options|
+ signed_blob_id = representation.blob.signed_id
+ variation_key = representation.variation.key
+ filename = representation.blob.filename
- route_for(:rails_blob_variation, signed_blob_id, variation_key, filename)
+ route_for(:rails_blob_representation, signed_blob_id, variation_key, filename, options)
end
- resolve("ActiveStorage::Variant") { |variant| route_for(:rails_variant, variant) }
+ resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_representation, variant, options) }
+ resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_representation, preview, options) }
- get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service, internal: true
- put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service, internal: true
- post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads, internal: true
+ get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
+ put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
+ post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
end
diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb
index ccc1d4a163..e1deee1d82 100644
--- a/activestorage/lib/active_storage.rb
+++ b/activestorage/lib/active_storage.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2017 David Heinemeier Hansson
+# Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -26,13 +26,27 @@
require "active_record"
require "active_support"
require "active_support/rails"
-require_relative "active_storage/version"
+
+require "active_storage/version"
+require "active_storage/errors"
+
+require "marcel"
module ActiveStorage
extend ActiveSupport::Autoload
autoload :Attached
autoload :Service
+ autoload :Previewer
+ autoload :Analyzer
+ mattr_accessor :logger
mattr_accessor :verifier
+ mattr_accessor :queue
+ mattr_accessor :previewers, default: []
+ mattr_accessor :analyzers, default: []
+ mattr_accessor :variant_processor, default: :mini_magick
+ mattr_accessor :paths, default: {}
+ mattr_accessor :variable_content_types, default: []
+ mattr_accessor :content_types_to_serve_as_binary, default: []
end
diff --git a/activestorage/lib/active_storage/analyzer.rb b/activestorage/lib/active_storage/analyzer.rb
new file mode 100644
index 0000000000..7c4168c1a0
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require "active_storage/downloading"
+
+module ActiveStorage
+ # This is an abstract base class for analyzers, which extract metadata from blobs. See
+ # ActiveStorage::Analyzer::ImageAnalyzer for an example of a concrete subclass.
+ class Analyzer
+ include Downloading
+
+ attr_reader :blob
+
+ # Implement this method in a concrete subclass. Have it return true when given a blob from which
+ # the analyzer can extract metadata.
+ def self.accept?(blob)
+ false
+ end
+
+ def initialize(blob)
+ @blob = blob
+ end
+
+ # Override this method in a concrete subclass. Have it return a Hash of metadata.
+ def metadata
+ raise NotImplementedError
+ end
+
+ private
+ def logger #:doc:
+ ActiveStorage.logger
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/image_analyzer.rb b/activestorage/lib/active_storage/analyzer/image_analyzer.rb
new file mode 100644
index 0000000000..3b39de91be
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/image_analyzer.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Extracts width and height in pixels from an image blob.
+ #
+ # If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience.
+ #
+ # Example:
+ #
+ # ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
+ # # => { width: 4104, height: 2736 }
+ #
+ # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
+ # the {ImageMagick}[http://www.imagemagick.org] system library.
+ class Analyzer::ImageAnalyzer < Analyzer
+ def self.accept?(blob)
+ blob.image?
+ end
+
+ def metadata
+ read_image do |image|
+ if rotated_image?(image)
+ { width: image.height, height: image.width }
+ else
+ { width: image.width, height: image.height }
+ end
+ end
+ rescue LoadError
+ logger.info "Skipping image analysis because the mini_magick gem isn't installed"
+ {}
+ end
+
+ private
+ def read_image
+ download_blob_to_tempfile do |file|
+ require "mini_magick"
+ yield MiniMagick::Image.new(file.path)
+ end
+ end
+
+ def rotated_image?(image)
+ %w[ RightTop LeftBottom ].include?(image["%[orientation]"])
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/null_analyzer.rb b/activestorage/lib/active_storage/analyzer/null_analyzer.rb
new file mode 100644
index 0000000000..8ff7ce48e5
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/null_analyzer.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Analyzer::NullAnalyzer < Analyzer # :nodoc:
+ def self.accept?(blob)
+ true
+ end
+
+ def metadata
+ {}
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
new file mode 100644
index 0000000000..e31bdb0edb
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Extracts the following from a video blob:
+ #
+ # * Width (pixels)
+ # * Height (pixels)
+ # * Duration (seconds)
+ # * Angle (degrees)
+ # * Display aspect ratio
+ #
+ # Example:
+ #
+ # ActiveStorage::VideoAnalyzer.new(blob).metadata
+ # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] }
+ #
+ # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
+ #
+ # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
+ class Analyzer::VideoAnalyzer < Analyzer
+ def self.accept?(blob)
+ blob.video?
+ end
+
+ def metadata
+ { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact
+ end
+
+ private
+ def width
+ if rotated?
+ computed_height || encoded_height
+ else
+ encoded_width
+ end
+ end
+
+ def height
+ if rotated?
+ encoded_width
+ else
+ computed_height || encoded_height
+ end
+ end
+
+ def duration
+ Float(video_stream["duration"]) if video_stream["duration"]
+ end
+
+ def angle
+ Integer(tags["rotate"]) if tags["rotate"]
+ end
+
+ def display_aspect_ratio
+ if descriptor = video_stream["display_aspect_ratio"]
+ if terms = descriptor.split(":", 2)
+ numerator = Integer(terms[0])
+ denominator = Integer(terms[1])
+
+ [numerator, denominator] unless numerator == 0
+ end
+ end
+ end
+
+
+ def rotated?
+ angle == 90 || angle == 270
+ end
+
+ def computed_height
+ if encoded_width && display_height_scale
+ encoded_width * display_height_scale
+ end
+ end
+
+ def encoded_width
+ @encoded_width ||= Float(video_stream["width"]) if video_stream["width"]
+ end
+
+ def encoded_height
+ @encoded_height ||= Float(video_stream["height"]) if video_stream["height"]
+ end
+
+ def display_height_scale
+ @display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio
+ end
+
+
+ def tags
+ @tags ||= video_stream["tags"] || {}
+ end
+
+ def video_stream
+ @video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
+ end
+
+ def streams
+ probe["streams"] || []
+ end
+
+ def probe
+ download_blob_to_tempfile { |file| probe_from(file) }
+ end
+
+ def probe_from(file)
+ IO.popen([ ffprobe_path, "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output|
+ JSON.parse(output.read)
+ end
+ rescue Errno::ENOENT
+ logger.info "Skipping video analysis because ffmpeg isn't installed"
+ {}
+ end
+
+ def ffprobe_path
+ ActiveStorage.paths[:ffprobe] || "ffprobe"
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached/macros.rb b/activestorage/lib/active_storage/attached/macros.rb
index 35a081adc4..819f00cc06 100644
--- a/activestorage/lib/active_storage/attached/macros.rb
+++ b/activestorage/lib/active_storage/attached/macros.rb
@@ -12,6 +12,10 @@ module ActiveStorage
# There is no column defined on the model side, Active Storage takes
# care of the mapping between your records and the attachment.
#
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
+ #
+ # User.with_attached_avatar
+ #
# Under the covers, this relationship is implemented as a +has_one+ association to a
# ActiveStorage::Attachment record and a +has_one-through+ association to a
# ActiveStorage::Blob record. These associations are available as +avatar_attachment+
@@ -28,13 +32,21 @@ module ActiveStorage
def #{name}
@active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"})
end
+
+ def #{name}=(attachable)
+ #{name}.attach(attachable)
+ end
CODE
- has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record
+ has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: false
has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob
+ scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }
+
if dependent == :purge_later
- before_destroy { public_send(name).purge_later }
+ after_destroy_commit { public_send(name).purge_later }
+ else
+ before_destroy { public_send(name).detach }
end
end
@@ -67,15 +79,31 @@ module ActiveStorage
def #{name}
@active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"})
end
+
+ def #{name}=(attachables)
+ #{name}.attach(attachables)
+ end
CODE
- has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment"
+ has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: false do
+ def purge
+ each(&:purge)
+ reset
+ end
+
+ def purge_later
+ each(&:purge_later)
+ reset
+ end
+ end
has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob
scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
if dependent == :purge_later
- before_destroy { public_send(name).purge_later }
+ after_destroy_commit { public_send(name).purge_later }
+ else
+ before_destroy { public_send(name).detach }
end
end
end
diff --git a/activestorage/lib/active_storage/attached/many.rb b/activestorage/lib/active_storage/attached/many.rb
index 1e0657c33c..d61acb6fad 100644
--- a/activestorage/lib/active_storage/attached/many.rb
+++ b/activestorage/lib/active_storage/attached/many.rb
@@ -13,7 +13,6 @@ module ActiveStorage
end
# Associates one or several attachments with the current record, saving them to the database.
- # Examples:
#
# document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects
# document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
@@ -21,7 +20,11 @@ module ActiveStorage
# document.images.attach([ first_blob, second_blob ])
def attach(*attachables)
attachables.flatten.collect do |attachable|
- attachments.create!(name: name, blob: create_blob_from(attachable))
+ if record.new_record?
+ attachments.build(record: record, blob: create_blob_from(attachable))
+ else
+ attachments.create!(record: record, blob: create_blob_from(attachable))
+ end
end
end
@@ -36,20 +39,21 @@ module ActiveStorage
attachments.any?
end
+ # Deletes associated attachments without purging them, leaving their respective blobs in place.
+ def detach
+ attachments.destroy_all if attached?
+ end
+
+ ##
+ # :method: purge
+ #
# Directly purges each associated attachment (i.e. destroys the blobs and
# attachments and deletes the files on the service).
- def purge
- if attached?
- attachments.each(&:purge)
- attachments.reload
- end
- end
+
+ ##
+ # :method: purge_later
+ #
# Purges each associated attachment through the queuing system.
- def purge_later
- if attached?
- attachments.each(&:purge_later)
- end
- end
end
end
diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb
index c66be08f58..f992cb5f84 100644
--- a/activestorage/lib/active_storage/attached/one.rb
+++ b/activestorage/lib/active_storage/attached/one.rb
@@ -13,18 +13,27 @@ module ActiveStorage
record.public_send("#{name}_attachment")
end
+ def blank?
+ attachment.blank?
+ end
+
# Associates a given attachment with the current record, saving it to the database.
- # Examples:
#
# person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object
# person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
# person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
# person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
def attach(attachable)
- if attached? && dependent == :purge_later
- replace attachable
- else
- write_attachment create_attachment_from(attachable)
+ blob_was = blob if attached?
+ blob = create_blob_from(attachable)
+
+ unless blob == blob_was
+ transaction do
+ detach
+ write_attachment build_attachment(blob: blob)
+ end
+
+ blob_was.purge_later if blob_was && dependent == :purge_later
end
end
@@ -39,6 +48,14 @@ module ActiveStorage
attachment.present?
end
+ # Deletes the attachment without purging it, leaving its blob in place.
+ def detach
+ if attached?
+ attachment.destroy
+ write_attachment nil
+ end
+ end
+
# Directly purges the attachment (i.e. destroys the blob and
# attachment and deletes the file on the service).
def purge
@@ -56,17 +73,10 @@ module ActiveStorage
end
private
- def replace(attachable)
- blob.tap do
- transaction do
- destroy
- write_attachment create_attachment_from(attachable)
- end
- end.purge_later
- end
+ delegate :transaction, to: :record
- def create_attachment_from(attachable)
- ActiveStorage::Attachment.create!(record: record, name: name, blob: create_blob_from(attachable))
+ def build_attachment(blob:)
+ ActiveStorage::Attachment.new(record: record, name: name, blob: blob)
end
def write_attachment(attachment)
diff --git a/activestorage/lib/active_storage/downloading.rb b/activestorage/lib/active_storage/downloading.rb
new file mode 100644
index 0000000000..7c3d20ade0
--- /dev/null
+++ b/activestorage/lib/active_storage/downloading.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require "tmpdir"
+
+module ActiveStorage
+ module Downloading
+ private
+ # Opens a new tempfile in #tempdir and copies blob data into it. Yields the tempfile.
+ def download_blob_to_tempfile #:doc:
+ open_tempfile_for_blob do |file|
+ download_blob_to file
+ yield file
+ end
+ end
+
+ def open_tempfile_for_blob
+ tempfile = Tempfile.open([ "ActiveStorage", blob.filename.extension_with_delimiter ], tempdir)
+
+ begin
+ yield tempfile
+ ensure
+ tempfile.close!
+ end
+ end
+
+ # Efficiently downloads blob data into the given file.
+ def download_blob_to(file) #:doc:
+ file.binmode
+ blob.download { |chunk| file.write(chunk) }
+ file.flush
+ file.rewind
+ end
+
+ # Returns the directory in which tempfiles should be opened. Defaults to +Dir.tmpdir+.
+ def tempdir #:doc:
+ Dir.tmpdir
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb
index 590a36a30a..99588cdd4b 100644
--- a/activestorage/lib/active_storage/engine.rb
+++ b/activestorage/lib/active_storage/engine.rb
@@ -3,19 +3,55 @@
require "rails"
require "active_storage"
+require "active_storage/previewer/poppler_pdf_previewer"
+require "active_storage/previewer/mupdf_previewer"
+require "active_storage/previewer/video_previewer"
+
+require "active_storage/analyzer/image_analyzer"
+require "active_storage/analyzer/video_analyzer"
+
module ActiveStorage
class Engine < Rails::Engine # :nodoc:
isolate_namespace ActiveStorage
config.active_storage = ActiveSupport::OrderedOptions.new
+ config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
+ config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
+ config.active_storage.paths = ActiveSupport::OrderedOptions.new
- config.eager_load_namespaces << ActiveStorage
+ config.active_storage.variable_content_types = %w(
+ image/png
+ image/gif
+ image/jpg
+ image/jpeg
+ image/vnd.adobe.photoshop
+ image/vnd.microsoft.icon
+ )
- initializer "active_storage.logger" do
- require "active_storage/service"
+ config.active_storage.content_types_to_serve_as_binary = %w(
+ text/html
+ text/javascript
+ image/svg+xml
+ application/postscript
+ application/x-shockwave-flash
+ text/xml
+ application/xml
+ application/xhtml+xml
+ )
+ config.eager_load_namespaces << ActiveStorage
+
+ initializer "active_storage.configs" do
config.after_initialize do |app|
- ActiveStorage::Service.logger = app.config.active_storage.logger || Rails.logger
+ ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
+ ActiveStorage.queue = app.config.active_storage.queue
+ ActiveStorage.variant_processor = app.config.active_storage.variant_processor || :mini_magick
+ ActiveStorage.previewers = app.config.active_storage.previewers || []
+ ActiveStorage.analyzers = app.config.active_storage.analyzers || []
+ ActiveStorage.paths = app.config.active_storage.paths || {}
+
+ ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
+ ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
end
end
@@ -34,7 +70,7 @@ module ActiveStorage
end
initializer "active_storage.services" do
- config.to_prepare do
+ ActiveSupport.on_load(:active_storage_blob) do
if config_choice = Rails.configuration.active_storage.service
configs = Rails.configuration.active_storage.service_configurations ||= begin
config_file = Pathname.new(Rails.root.join("config/storage.yml"))
diff --git a/activestorage/lib/active_storage/errors.rb b/activestorage/lib/active_storage/errors.rb
new file mode 100644
index 0000000000..f099b13f5b
--- /dev/null
+++ b/activestorage/lib/active_storage/errors.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class InvariableError < StandardError; end
+ class UnpreviewableError < StandardError; end
+ class UnrepresentableError < StandardError; end
+end
diff --git a/activestorage/lib/active_storage/gem_version.rb b/activestorage/lib/active_storage/gem_version.rb
index e1d7b3493a..492620731b 100644
--- a/activestorage/lib/active_storage/gem_version.rb
+++ b/activestorage/lib/active_storage/gem_version.rb
@@ -7,8 +7,8 @@ module ActiveStorage
end
module VERSION
- MAJOR = 5
- MINOR = 2
+ MAJOR = 6
+ MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/activestorage/lib/active_storage/log_subscriber.rb b/activestorage/lib/active_storage/log_subscriber.rb
index 0d00a75c0e..a4e148c1a5 100644
--- a/activestorage/lib/active_storage/log_subscriber.rb
+++ b/activestorage/lib/active_storage/log_subscriber.rb
@@ -18,6 +18,10 @@ module ActiveStorage
info event, color("Deleted file from key: #{key_in(event)}", RED)
end
+ def service_delete_prefixed(event)
+ info event, color("Deleted files by key prefix: #{event.payload[:prefix]}", RED)
+ end
+
def service_exist(event)
debug event, color("Checked if file exists at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE)
end
@@ -27,7 +31,7 @@ module ActiveStorage
end
def logger
- ActiveStorage::Service.logger
+ ActiveStorage.logger
end
private
diff --git a/activestorage/lib/active_storage/previewer.rb b/activestorage/lib/active_storage/previewer.rb
new file mode 100644
index 0000000000..cf19987d72
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require "active_storage/downloading"
+
+module ActiveStorage
+ # This is an abstract base class for previewers, which generate images from blobs. See
+ # ActiveStorage::Previewer::PDFPreviewer and ActiveStorage::Previewer::VideoPreviewer for examples of
+ # concrete subclasses.
+ class Previewer
+ include Downloading
+
+ attr_reader :blob
+
+ # Implement this method in a concrete subclass. Have it return true when given a blob from which
+ # the previewer can generate an image.
+ def self.accept?(blob)
+ false
+ end
+
+ def initialize(blob)
+ @blob = blob
+ end
+
+ # Override this method in a concrete subclass. Have it yield an attachable preview image (i.e.
+ # anything accepted by ActiveStorage::Attached::One#attach).
+ def preview
+ raise NotImplementedError
+ end
+
+ private
+ # Executes a system command, capturing its binary output in a tempfile. Yields the tempfile.
+ #
+ # Use this method to shell out to a system library (e.g. mupdf or ffmpeg) for preview image
+ # generation. The resulting tempfile can be used as the +:io+ value in an attachable Hash:
+ #
+ # def preview
+ # download_blob_to_tempfile do |input|
+ # draw "my-drawing-command", input.path, "--format", "png", "-" do |output|
+ # yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ # end
+ # end
+ # end
+ #
+ # The output tempfile is opened in the directory returned by ActiveStorage::Downloading#tempdir.
+ def draw(*argv) #:doc:
+ ActiveSupport::Notifications.instrument("preview.active_storage") do
+ open_tempfile_for_drawing do |file|
+ capture(*argv, to: file)
+ yield file
+ end
+ end
+ end
+
+ def open_tempfile_for_drawing
+ tempfile = Tempfile.open("ActiveStorage", tempdir)
+
+ begin
+ yield tempfile
+ ensure
+ tempfile.close!
+ end
+ end
+
+ def capture(*argv, to:)
+ to.binmode
+ IO.popen(argv, err: File::NULL) { |out| IO.copy_stream(out, to) }
+ to.rewind
+ end
+
+ def logger #:doc:
+ ActiveStorage.logger
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/previewer/mupdf_previewer.rb b/activestorage/lib/active_storage/previewer/mupdf_previewer.rb
new file mode 100644
index 0000000000..ae02a4889d
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer/mupdf_previewer.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Previewer::MuPDFPreviewer < Previewer
+ class << self
+ def accept?(blob)
+ blob.content_type == "application/pdf" && mutool_exists?
+ end
+
+ def mutool_path
+ ActiveStorage.paths[:mutool] || "mutool"
+ end
+
+ def mutool_exists?
+ return @mutool_exists unless @mutool_exists.nil?
+
+ system mutool_path, out: File::NULL, err: File::NULL
+
+ @mutool_exists = $?.exitstatus == 1
+ end
+ end
+
+ def preview
+ download_blob_to_tempfile do |input|
+ draw_first_page_from input do |output|
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ end
+ end
+ end
+
+ private
+ def draw_first_page_from(file, &block)
+ draw self.class.mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb
new file mode 100644
index 0000000000..2a787362cf
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Previewer::PopplerPDFPreviewer < Previewer
+ class << self
+ def accept?(blob)
+ blob.content_type == "application/pdf" && pdftoppm_exists?
+ end
+
+ def pdftoppm_path
+ ActiveStorage.paths[:pdftoppm] || "pdftoppm"
+ end
+
+ def pdftoppm_exists?
+ return @pdftoppm_exists unless @pdftoppm_exists.nil?
+
+ @pdftoppm_exists = system(pdftoppm_path, "-v", out: File::NULL, err: File::NULL)
+ end
+ end
+
+ def preview
+ download_blob_to_tempfile do |input|
+ draw_first_page_from input do |output|
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ end
+ end
+ end
+
+ private
+ def draw_first_page_from(file, &block)
+ # use 72 dpi to match thumbnail dimesions of the PDF
+ draw self.class.pdftoppm_path, "-singlefile", "-r", "72", "-png", file.path, &block
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/previewer/video_previewer.rb b/activestorage/lib/active_storage/previewer/video_previewer.rb
new file mode 100644
index 0000000000..2f28a3d341
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer/video_previewer.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Previewer::VideoPreviewer < Previewer
+ def self.accept?(blob)
+ blob.video?
+ end
+
+ def preview
+ download_blob_to_tempfile do |input|
+ draw_relevant_frame_from input do |output|
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ end
+ end
+ end
+
+ private
+ def draw_relevant_frame_from(file, &block)
+ draw ffmpeg_path, "-i", file.path, "-y", "-vcodec", "png",
+ "-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-", &block
+ end
+
+ def ffmpeg_path
+ ActiveStorage.paths[:ffmpeg] || "ffmpeg"
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb
index b80fdea1ab..949969fc95 100644
--- a/activestorage/lib/active_storage/service.rb
+++ b/activestorage/lib/active_storage/service.rb
@@ -4,6 +4,7 @@ require "active_storage/log_subscriber"
module ActiveStorage
class IntegrityError < StandardError; end
+
# Abstract class serving as an interface for concrete services.
#
# The available services are:
@@ -40,7 +41,7 @@ module ActiveStorage
extend ActiveSupport::Autoload
autoload :Configurator
- class_attribute :logger
+ class_attribute :url_expires_in, default: 5.minutes
class << self
# Configure an Active Storage service by name from a set of configurations,
@@ -72,11 +73,21 @@ module ActiveStorage
raise NotImplementedError
end
+ # Return the partial content in the byte +range+ of the file at the +key+.
+ def download_chunk(key, range)
+ raise NotImplementedError
+ end
+
# Delete the file at the +key+.
def delete(key)
raise NotImplementedError
end
+ # Delete files at keys starting with the +prefix+.
+ def delete_prefixed(prefix)
+ raise NotImplementedError
+ end
+
# Return +true+ if a file exists at the +key+.
def exist?(key)
raise NotImplementedError
@@ -91,7 +102,7 @@ module ActiveStorage
# Returns a signed, temporary URL that a direct upload file can be PUT to on the +key+.
# The URL will be valid for the amount of seconds specified in +expires_in+.
- # You most also provide the +content_type+, +content_length+, and +checksum+ of the file
+ # You must also provide the +content_type+, +content_length+, and +checksum+ of the file
# that will be uploaded. All these attributes will be validated by the service upon upload.
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
raise NotImplementedError
@@ -103,15 +114,19 @@ module ActiveStorage
end
private
- def instrument(operation, key, payload = {}, &block)
+ def instrument(operation, payload = {}, &block)
ActiveSupport::Notifications.instrument(
"service_#{operation}.active_storage",
- payload.merge(key: key, service: service_name), &block)
+ payload.merge(service: service_name), &block)
end
def service_name
# ActiveStorage::Service::DiskService => Disk
self.class.name.split("::").third.remove("Service")
end
+
+ def content_disposition_with(type: "inline", filename:)
+ (type.to_s.presence_in(%w( attachment inline )) || "inline") + "; #{filename.parameters}"
+ end
end
end
diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb
index 895cc9c2f1..2867a4e441 100644
--- a/activestorage/lib/active_storage/service/azure_storage_service.rb
+++ b/activestorage/lib/active_storage/service/azure_storage_service.rb
@@ -8,18 +8,17 @@ module ActiveStorage
# Wraps the Microsoft Azure Storage Blob Service as an Active Storage service.
# See ActiveStorage::Service for the generic API documentation that applies to all services.
class Service::AzureStorageService < Service
- attr_reader :client, :path, :blobs, :container, :signer
+ attr_reader :client, :blobs, :container, :signer
- def initialize(path:, storage_account_name:, storage_access_key:, container:)
+ def initialize(storage_account_name:, storage_access_key:, container:)
@client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key)
@signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
@blobs = client.blob_client
@container = container
- @path = path
end
def upload(key, io, checksum: nil)
- instrument :upload, key, checksum: checksum do
+ instrument :upload, key: key, checksum: checksum do
begin
blobs.create_block_blob(container, key, io, content_md5: checksum)
rescue Azure::Core::Http::HTTPError
@@ -28,31 +27,54 @@ module ActiveStorage
end
end
- def download(key)
+ def download(key, &block)
if block_given?
- instrument :streaming_download, key do
+ instrument :streaming_download, key: key do
stream(key, &block)
end
else
- instrument :download, key do
+ instrument :download, key: key do
_, io = blobs.get_blob(container, key)
io.force_encoding(Encoding::BINARY)
end
end
end
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ _, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
+ io.force_encoding(Encoding::BINARY)
+ end
+ end
+
def delete(key)
- instrument :delete, key do
+ instrument :delete, key: key do
begin
blobs.delete_blob(container, key)
rescue Azure::Core::Http::HTTPError
- false
+ # Ignore files already deleted
+ end
+ end
+ end
+
+ def delete_prefixed(prefix)
+ instrument :delete_prefixed, prefix: prefix do
+ marker = nil
+
+ loop do
+ results = blobs.list_blobs(container, prefix: prefix, marker: marker)
+
+ results.each do |blob|
+ blobs.delete_blob(container, blob.name)
+ end
+
+ break unless marker = results.continuation_token.presence
end
end
end
def exist?(key)
- instrument :exist, key do |payload|
+ instrument :exist, key: key do |payload|
answer = blob_for(key).present?
payload[:exist] = answer
answer
@@ -60,13 +82,13 @@ module ActiveStorage
end
def url(key, expires_in:, filename:, disposition:, content_type:)
- instrument :url, key do |payload|
- base_url = url_for(key)
+ instrument :url, key: key do |payload|
generated_url = signer.signed_uri(
- URI(base_url), false,
+ uri_for(key), false,
+ service: "b",
permissions: "r",
expiry: format_expiry(expires_in),
- content_disposition: disposition,
+ content_disposition: content_disposition_with(type: disposition, filename: filename),
content_type: content_type
).to_s
@@ -77,10 +99,13 @@ module ActiveStorage
end
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
- instrument :url, key do |payload|
- base_url = url_for(key)
- generated_url = signer.signed_uri(URI(base_url), false, permissions: "rw",
- expiry: format_expiry(expires_in)).to_s
+ instrument :url, key: key do |payload|
+ generated_url = signer.signed_uri(
+ uri_for(key), false,
+ service: "b",
+ permissions: "rw",
+ expiry: format_expiry(expires_in)
+ ).to_s
payload[:url] = generated_url
@@ -93,8 +118,8 @@ module ActiveStorage
end
private
- def url_for(key)
- "#{path}/#{container}/#{key}"
+ def uri_for(key)
+ blobs.generate_uri("#{container}/#{key}")
end
def blob_for(key)
@@ -108,15 +133,15 @@ module ActiveStorage
end
# Reads the object for the given key in chunks, yielding each to the block.
- def stream(key, options = {}, &block)
+ def stream(key)
blob = blob_for(key)
chunk_size = 5.megabytes
offset = 0
while offset < blob.properties[:content_length]
- _, io = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
- yield io
+ _, chunk = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
+ yield chunk.force_encoding(Encoding::BINARY)
offset += chunk_size
end
end
diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb
index f600753a08..5b652fe74e 100644
--- a/activestorage/lib/active_storage/service/disk_service.rb
+++ b/activestorage/lib/active_storage/service/disk_service.rb
@@ -16,7 +16,7 @@ module ActiveStorage
end
def upload(key, io, checksum: nil)
- instrument :upload, key, checksum: checksum do
+ instrument :upload, key: key, checksum: checksum do
IO.copy_stream(io, make_path_for(key))
ensure_integrity_of(key, checksum) if checksum
end
@@ -24,7 +24,7 @@ module ActiveStorage
def download(key)
if block_given?
- instrument :streaming_download, key do
+ instrument :streaming_download, key: key do
File.open(path_for(key), "rb") do |file|
while data = file.read(64.kilobytes)
yield data
@@ -32,14 +32,23 @@ module ActiveStorage
end
end
else
- instrument :download, key do
+ instrument :download, key: key do
File.binread path_for(key)
end
end
end
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ File.open(path_for(key), "rb") do |file|
+ file.seek range.begin
+ file.read range.size
+ end
+ end
+ end
+
def delete(key)
- instrument :delete, key do
+ instrument :delete, key: key do
begin
File.delete path_for(key)
rescue Errno::ENOENT
@@ -48,8 +57,16 @@ module ActiveStorage
end
end
+ def delete_prefixed(prefix)
+ instrument :delete_prefixed, prefix: prefix do
+ Dir.glob(path_for("#{prefix}*")).each do |path|
+ FileUtils.rm_rf(path)
+ end
+ end
+ end
+
def exist?(key)
- instrument :exist, key do |payload|
+ instrument :exist, key: key do |payload|
answer = File.exist? path_for(key)
payload[:exist] = answer
answer
@@ -57,17 +74,17 @@ module ActiveStorage
end
def url(key, expires_in:, filename:, disposition:, content_type:)
- instrument :url, key do |payload|
+ instrument :url, key: key do |payload|
verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key)
generated_url =
- if defined?(Rails.application)
- Rails.application.routes.url_helpers.rails_disk_service_path \
- verified_key_with_expiration,
- filename: filename, disposition: disposition, content_type: content_type
- else
- "/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?content_type=#{content_type}&disposition=#{disposition}"
- end
+ url_helpers.rails_disk_service_url(
+ verified_key_with_expiration,
+ host: current_host,
+ filename: filename,
+ disposition: content_disposition_with(type: disposition, filename: filename),
+ content_type: content_type
+ )
payload[:url] = generated_url
@@ -76,7 +93,7 @@ module ActiveStorage
end
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
- instrument :url, key do |payload|
+ instrument :url, key: key do |payload|
verified_token_with_expiration = ActiveStorage.verifier.generate(
{
key: key,
@@ -88,12 +105,7 @@ module ActiveStorage
purpose: :blob_token }
)
- generated_url =
- if defined?(Rails.application)
- Rails.application.routes.url_helpers.update_rails_disk_service_path verified_token_with_expiration
- else
- "/rails/active_storage/disk/#{verified_token_with_expiration}"
- end
+ generated_url = url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: current_host)
payload[:url] = generated_url
@@ -124,5 +136,13 @@ module ActiveStorage
raise ActiveStorage::IntegrityError
end
end
+
+ def url_helpers
+ @url_helpers ||= Rails.application.routes.url_helpers
+ end
+
+ def current_host
+ ActiveStorage::Current.host
+ end
end
end
diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb
index 685dd61a0a..16a0765fc5 100644
--- a/activestorage/lib/active_storage/service/gcs_service.rb
+++ b/activestorage/lib/active_storage/service/gcs_service.rb
@@ -1,40 +1,64 @@
# frozen_string_literal: true
+gem "google-cloud-storage", "~> 1.8"
+
require "google/cloud/storage"
+require "net/http"
+
require "active_support/core_ext/object/to_query"
+require "active_storage/filename"
module ActiveStorage
# Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API
# documentation that applies to all services.
class Service::GCSService < Service
- attr_reader :client, :bucket
-
- def initialize(project:, keyfile:, bucket:, **options)
- @client = Google::Cloud::Storage.new(project: project, keyfile: keyfile, **options)
- @bucket = @client.bucket(bucket)
+ def initialize(**config)
+ @config = config
end
def upload(key, io, checksum: nil)
- instrument :upload, key, checksum: checksum do
+ instrument :upload, key: key, checksum: checksum do
begin
- bucket.create_file(io, key, md5: checksum)
+ # The official GCS client library doesn't allow us to create a file with no Content-Type metadata.
+ # We need the file we create to have no Content-Type so we can control it via the response-content-type
+ # param in signed URLs. Workaround: let the GCS client create the file with an inferred
+ # Content-Type (usually "application/octet-stream") then clear it.
+ bucket.create_file(io, key, md5: checksum).update do |file|
+ file.content_type = nil
+ end
rescue Google::Cloud::InvalidArgumentError
raise ActiveStorage::IntegrityError
end
end
end
- # FIXME: Add streaming when given a block
+ # FIXME: Download in chunks when given a block.
def download(key)
- instrument :download, key do
+ instrument :download, key: key do
io = file_for(key).download
io.rewind
- io.read
+
+ if block_given?
+ yield io.read
+ else
+ io.read
+ end
+ end
+ end
+
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ file = file_for(key)
+ uri = URI(file.signed_url(expires: 30.seconds))
+
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |client|
+ client.get(uri, "Range" => "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body
+ end
end
end
def delete(key)
- instrument :delete, key do
+ instrument :delete, key: key do
begin
file_for(key).delete
rescue Google::Cloud::NotFoundError
@@ -43,8 +67,14 @@ module ActiveStorage
end
end
+ def delete_prefixed(prefix)
+ instrument :delete_prefixed, prefix: prefix do
+ bucket.files(prefix: prefix).all(&:delete)
+ end
+ end
+
def exist?(key)
- instrument :exist, key do |payload|
+ instrument :exist, key: key do |payload|
answer = file_for(key).exists?
payload[:exist] = answer
answer
@@ -52,9 +82,9 @@ module ActiveStorage
end
def url(key, expires_in:, filename:, content_type:, disposition:)
- instrument :url, key do |payload|
+ instrument :url, key: key do |payload|
generated_url = file_for(key).signed_url expires: expires_in, query: {
- "response-content-disposition" => disposition,
+ "response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
"response-content-type" => content_type
}
@@ -64,10 +94,9 @@ module ActiveStorage
end
end
- def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
- instrument :url, key do |payload|
- generated_url = bucket.signed_url key, method: "PUT", expires: expires_in,
- content_type: content_type, content_md5: checksum
+ def url_for_direct_upload(key, expires_in:, checksum:, **)
+ instrument :url, key: key do |payload|
+ generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, content_md5: checksum
payload[:url] = generated_url
@@ -75,13 +104,23 @@ module ActiveStorage
end
end
- def headers_for_direct_upload(key, content_type:, checksum:, **)
- { "Content-Type" => content_type, "Content-MD5" => checksum }
+ def headers_for_direct_upload(key, checksum:, **)
+ { "Content-MD5" => checksum }
end
private
+ attr_reader :config
+
def file_for(key)
bucket.file(key, skip_lookup: true)
end
+
+ def bucket
+ @bucket ||= client.bucket(config.fetch(:bucket))
+ end
+
+ def client
+ @client ||= Google::Cloud::Storage.new(config.except(:bucket))
+ end
end
end
diff --git a/activestorage/lib/active_storage/service/mirror_service.rb b/activestorage/lib/active_storage/service/mirror_service.rb
index 39e922f7ab..6002ef5a00 100644
--- a/activestorage/lib/active_storage/service/mirror_service.rb
+++ b/activestorage/lib/active_storage/service/mirror_service.rb
@@ -9,7 +9,7 @@ module ActiveStorage
class Service::MirrorService < Service
attr_reader :primary, :mirrors
- delegate :download, :exist?, :url, to: :primary
+ delegate :download, :download_chunk, :exist?, :url, to: :primary
# Stitch together from named services.
def self.build(primary:, mirrors:, configurator:, **options) #:nodoc:
@@ -35,6 +35,11 @@ module ActiveStorage
perform_across_services :delete, key
end
+ # Delete files at keys starting with the +prefix+ on all services.
+ def delete_prefixed(prefix)
+ perform_across_services :delete_prefixed, prefix
+ end
+
private
def each_service(&block)
[ primary, *mirrors ].each(&block)
diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb
index e074269353..0286e7ff21 100644
--- a/activestorage/lib/active_storage/service/s3_service.rb
+++ b/activestorage/lib/active_storage/service/s3_service.rb
@@ -9,15 +9,15 @@ module ActiveStorage
class Service::S3Service < Service
attr_reader :client, :bucket, :upload_options
- def initialize(access_key_id:, secret_access_key:, region:, bucket:, upload: {}, **options)
- @client = Aws::S3::Resource.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region, **options)
+ def initialize(bucket:, upload: {}, **options)
+ @client = Aws::S3::Resource.new(**options)
@bucket = @client.bucket(bucket)
@upload_options = upload
end
def upload(key, io, checksum: nil)
- instrument :upload, key, checksum: checksum do
+ instrument :upload, key: key, checksum: checksum do
begin
object_for(key).put(upload_options.merge(body: io, content_md5: checksum))
rescue Aws::S3::Errors::BadDigest
@@ -26,26 +26,38 @@ module ActiveStorage
end
end
- def download(key)
+ def download(key, &block)
if block_given?
- instrument :streaming_download, key do
+ instrument :streaming_download, key: key do
stream(key, &block)
end
else
- instrument :download, key do
- object_for(key).get.body.read.force_encoding(Encoding::BINARY)
+ instrument :download, key: key do
+ object_for(key).get.body.string.force_encoding(Encoding::BINARY)
end
end
end
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body.read.force_encoding(Encoding::BINARY)
+ end
+ end
+
def delete(key)
- instrument :delete, key do
+ instrument :delete, key: key do
object_for(key).delete
end
end
+ def delete_prefixed(prefix)
+ instrument :delete_prefixed, prefix: prefix do
+ bucket.objects(prefix: prefix).batch_delete!
+ end
+ end
+
def exist?(key)
- instrument :exist, key do |payload|
+ instrument :exist, key: key do |payload|
answer = object_for(key).exists?
payload[:exist] = answer
answer
@@ -53,9 +65,9 @@ module ActiveStorage
end
def url(key, expires_in:, filename:, disposition:, content_type:)
- instrument :url, key do |payload|
+ instrument :url, key: key do |payload|
generated_url = object_for(key).presigned_url :get, expires_in: expires_in.to_i,
- response_content_disposition: disposition,
+ response_content_disposition: content_disposition_with(type: disposition, filename: filename),
response_content_type: content_type
payload[:url] = generated_url
@@ -65,7 +77,7 @@ module ActiveStorage
end
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
- instrument :url, key do |payload|
+ instrument :url, key: key do |payload|
generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
content_type: content_type, content_length: content_length, content_md5: checksum
@@ -85,14 +97,14 @@ module ActiveStorage
end
# Reads the object for the given key in chunks, yielding each to the block.
- def stream(key, options = {}, &block)
+ def stream(key)
object = object_for(key)
chunk_size = 5.megabytes
offset = 0
while offset < object.content_length
- yield object.read(options.merge(range: "bytes=#{offset}-#{offset + chunk_size - 1}"))
+ yield object.get(range: "bytes=#{offset}-#{offset + chunk_size - 1}").body.read.force_encoding(Encoding::BINARY)
offset += chunk_size
end
end
diff --git a/activestorage/lib/tasks/activestorage.rake b/activestorage/lib/tasks/activestorage.rake
index ef923e5926..296e91afa1 100644
--- a/activestorage/lib/tasks/activestorage.rake
+++ b/activestorage/lib/tasks/activestorage.rake
@@ -3,6 +3,10 @@
namespace :active_storage do
desc "Copy over the migration needed to the application"
task install: :environment do
- Rake::Task["active_storage:install:migrations"].invoke
+ if Rake::Task.task_defined?("active_storage:install:migrations")
+ Rake::Task["active_storage:install:migrations"].invoke
+ else
+ Rake::Task["app:active_storage:install:migrations"].invoke
+ end
end
end
diff --git a/activestorage/package.json b/activestorage/package.json
index b481295a8d..00876985cf 100644
--- a/activestorage/package.json
+++ b/activestorage/package.json
@@ -1,10 +1,11 @@
{
"name": "activestorage",
- "version": "5.2.0.alpha",
+ "version": "6.0.0-alpha",
"description": "Attach cloud and local files in Rails applications",
"main": "app/assets/javascripts/activestorage.js",
"files": [
- "app/assets/javascripts/*.js"
+ "app/assets/javascripts/*.js",
+ "src/*.js"
],
"homepage": "http://rubyonrails.org/",
"repository": {
@@ -16,18 +17,25 @@
},
"author": "Javan Makhmali <javan@javan.us>",
"license": "MIT",
+ "dependencies": {
+ "spark-md5": "^3.0.0"
+ },
"devDependencies": {
"babel-core": "^6.25.0",
- "babel-loader": "^7.1.1",
+ "babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.6.0",
"eslint": "^4.3.0",
"eslint-plugin-import": "^2.7.0",
- "spark-md5": "^3.0.0",
- "webpack": "^3.4.0"
+ "rollup": "^0.58.2",
+ "rollup-plugin-babel": "^3.0.4",
+ "rollup-plugin-commonjs": "^9.1.0",
+ "rollup-plugin-node-resolve": "^3.3.0",
+ "rollup-plugin-uglify": "^3.0.0"
},
"scripts": {
"prebuild": "yarn lint",
- "build": "webpack -p",
- "lint": "eslint app/javascript"
+ "build": "rollup --config rollup.config.js",
+ "lint": "eslint app/javascript",
+ "prepublishOnly": "rm -rf src && cp -R app/javascript/activestorage src"
}
}
diff --git a/activestorage/rollup.config.js b/activestorage/rollup.config.js
new file mode 100644
index 0000000000..1b4f9477ab
--- /dev/null
+++ b/activestorage/rollup.config.js
@@ -0,0 +1,28 @@
+import resolve from "rollup-plugin-node-resolve"
+import commonjs from "rollup-plugin-commonjs"
+import babel from "rollup-plugin-babel"
+import uglify from "rollup-plugin-uglify"
+
+const uglifyOptions = {
+ mangle: false,
+ compress: false,
+ output: {
+ beautify: true,
+ indent_level: 2
+ }
+}
+
+export default {
+ input: "app/javascript/activestorage/index.js",
+ output: {
+ file: "app/assets/javascripts/activestorage.js",
+ format: "umd",
+ name: "ActiveStorage"
+ },
+ plugins: [
+ resolve(),
+ commonjs(),
+ babel(),
+ uglify(uglifyOptions)
+ ]
+}
diff --git a/activestorage/test/analyzer/image_analyzer_test.rb b/activestorage/test/analyzer/image_analyzer_test.rb
new file mode 100644
index 0000000000..55bb5e7280
--- /dev/null
+++ b/activestorage/test/analyzer/image_analyzer_test.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/analyzer/image_analyzer"
+
+class ActiveStorage::Analyzer::ImageAnalyzerTest < ActiveSupport::TestCase
+ test "analyzing a JPEG image" do
+ blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 4104, metadata[:width]
+ assert_equal 2736, metadata[:height]
+ end
+
+ test "analyzing a rotated JPEG image" do
+ blob = create_file_blob(filename: "racecar_rotated.jpg", content_type: "image/jpeg")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 2736, metadata[:width]
+ assert_equal 4104, metadata[:height]
+ end
+
+ test "analyzing an SVG image without an XML declaration" do
+ blob = create_file_blob(filename: "icon.svg", content_type: "image/svg+xml")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 792, metadata[:width]
+ assert_equal 584, metadata[:height]
+ end
+end
diff --git a/activestorage/test/analyzer/video_analyzer_test.rb b/activestorage/test/analyzer/video_analyzer_test.rb
new file mode 100644
index 0000000000..d30f49315a
--- /dev/null
+++ b/activestorage/test/analyzer/video_analyzer_test.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/analyzer/video_analyzer"
+
+class ActiveStorage::Analyzer::VideoAnalyzerTest < ActiveSupport::TestCase
+ test "analyzing a video" do
+ blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 640, metadata[:width]
+ assert_equal 480, metadata[:height]
+ assert_equal [4, 3], metadata[:display_aspect_ratio]
+ assert_equal 5.166648, metadata[:duration]
+ assert_not_includes metadata, :angle
+ end
+
+ test "analyzing a rotated video" do
+ blob = create_file_blob(filename: "rotated_video.mp4", content_type: "video/mp4")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 480, metadata[:width]
+ assert_equal 640, metadata[:height]
+ assert_equal [4, 3], metadata[:display_aspect_ratio]
+ assert_equal 5.227975, metadata[:duration]
+ assert_equal 90, metadata[:angle]
+ end
+
+ test "analyzing a video with rectangular samples" do
+ blob = create_file_blob(filename: "video_with_rectangular_samples.mp4", content_type: "video/mp4")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 1280, metadata[:width]
+ assert_equal 720, metadata[:height]
+ assert_equal [16, 9], metadata[:display_aspect_ratio]
+ end
+
+ test "analyzing a video with an undefined display aspect ratio" do
+ blob = create_file_blob(filename: "video_with_undefined_display_aspect_ratio.mp4", content_type: "video/mp4")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 640, metadata[:width]
+ assert_equal 480, metadata[:height]
+ assert_nil metadata[:display_aspect_ratio]
+ end
+
+ test "analyzing a video without a video stream" do
+ blob = create_file_blob(filename: "video_without_video_stream.mp4", content_type: "video/mp4")
+ metadata = extract_metadata_from(blob)
+ assert_equal({ "analyzed" => true, "identified" => true }, metadata)
+ end
+end
diff --git a/activestorage/test/controllers/blobs_controller_test.rb b/activestorage/test/controllers/blobs_controller_test.rb
index c37b9c8a10..9c811df895 100644
--- a/activestorage/test/controllers/blobs_controller_test.rb
+++ b/activestorage/test/controllers/blobs_controller_test.rb
@@ -5,7 +5,12 @@ require "database/setup"
class ActiveStorage::BlobsControllerTest < ActionDispatch::IntegrationTest
setup do
- @blob = create_image_blob filename: "racecar.jpg"
+ @blob = create_file_blob filename: "racecar.jpg"
+ end
+
+ test "showing blob with invalid signed ID" do
+ get rails_service_blob_url("invalid", "racecar.jpg")
+ assert_response :not_found
end
test "showing blob utilizes browser caching" do
diff --git a/activestorage/test/controllers/direct_uploads_controller_test.rb b/activestorage/test/controllers/direct_uploads_controller_test.rb
index 888767086c..1b16da17d9 100644
--- a/activestorage/test/controllers/direct_uploads_controller_test.rb
+++ b/activestorage/test/controllers/direct_uploads_controller_test.rb
@@ -27,7 +27,7 @@ if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].pr
assert_equal checksum, details["checksum"]
assert_equal "text/plain", details["content_type"]
assert_match SERVICE_CONFIGURATIONS[:s3][:bucket], details["direct_upload"]["url"]
- assert_match(/s3\.(\S+)?amazonaws\.com/, details["direct_upload"]["url"])
+ assert_match(/s3(-[-a-z0-9]+)?\.(\S+)?amazonaws\.com/, details["direct_upload"]["url"])
assert_equal({ "Content-Type" => "text/plain", "Content-MD5" => checksum }, details["direct_upload"]["headers"])
end
end
@@ -62,7 +62,7 @@ if SERVICE_CONFIGURATIONS[:gcs]
assert_equal checksum, details["checksum"]
assert_equal "text/plain", details["content_type"]
assert_match %r{storage\.googleapis\.com/#{@config[:bucket]}}, details["direct_upload"]["url"]
- assert_equal({ "Content-Type" => "text/plain", "Content-MD5" => checksum }, details["direct_upload"]["headers"])
+ assert_equal({ "Content-MD5" => checksum }, details["direct_upload"]["headers"])
end
end
end
@@ -121,4 +121,27 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati
assert_equal({ "Content-Type" => "text/plain" }, details["direct_upload"]["headers"])
end
end
+
+ test "creating new direct upload does not include root in json" do
+ checksum = Digest::MD5.base64digest("Hello")
+
+ set_include_root_in_json(true) do
+ post rails_direct_uploads_url, params: { blob: {
+ filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain" } }
+ end
+
+ @response.parsed_body.tap do |details|
+ assert_nil details["blob"]
+ assert_not_nil details["id"]
+ end
+ end
+
+ private
+ def set_include_root_in_json(value)
+ original = ActiveRecord::Base.include_root_in_json
+ ActiveRecord::Base.include_root_in_json = value
+ yield
+ ensure
+ ActiveRecord::Base.include_root_in_json = original
+ end
end
diff --git a/activestorage/test/controllers/representations_controller_test.rb b/activestorage/test/controllers/representations_controller_test.rb
new file mode 100644
index 0000000000..2662cc5283
--- /dev/null
+++ b/activestorage/test/controllers/representations_controller_test.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::RepresentationsControllerWithVariantsTest < ActionDispatch::IntegrationTest
+ setup do
+ @blob = create_file_blob filename: "racecar.jpg"
+ end
+
+ test "showing variant inline" do
+ get rails_blob_representation_url(
+ filename: @blob.filename,
+ signed_blob_id: @blob.signed_id,
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_redirected_to(/racecar\.jpg\?.*disposition=inline/)
+
+ image = read_image(@blob.variant(resize: "100x100"))
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ end
+
+ test "showing variant with invalid signed blob ID" do
+ get rails_blob_representation_url(
+ filename: @blob.filename,
+ signed_blob_id: "invalid",
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_response :not_found
+ end
+end
+
+class ActiveStorage::RepresentationsControllerWithPreviewsTest < ActionDispatch::IntegrationTest
+ setup do
+ @blob = create_file_blob filename: "report.pdf", content_type: "application/pdf"
+ end
+
+ test "showing preview inline" do
+ get rails_blob_representation_url(
+ filename: @blob.filename,
+ signed_blob_id: @blob.signed_id,
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_predicate @blob.preview_image, :attached?
+ assert_redirected_to(/report\.png\?.*disposition=inline/)
+
+ image = read_image(@blob.preview_image.variant(resize: "100x100"))
+ assert_equal 77, image.width
+ assert_equal 100, image.height
+ end
+
+ test "showing preview with invalid signed blob ID" do
+ get rails_blob_representation_url(
+ filename: @blob.filename,
+ signed_blob_id: "invalid",
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_response :not_found
+ end
+end
diff --git a/activestorage/test/controllers/variants_controller_test.rb b/activestorage/test/controllers/variants_controller_test.rb
deleted file mode 100644
index 0a049f3bc4..0000000000
--- a/activestorage/test/controllers/variants_controller_test.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "database/setup"
-
-class ActiveStorage::VariantsControllerTest < ActionDispatch::IntegrationTest
- setup do
- @blob = create_image_blob filename: "racecar.jpg"
- end
-
- test "showing variant inline" do
- get rails_blob_variation_url(
- filename: @blob.filename,
- signed_blob_id: @blob.signed_id,
- variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
-
- assert_redirected_to(/racecar\.jpg\?.*disposition=inline/)
-
- image = read_image_variant(@blob.variant(resize: "100x100"))
- assert_equal 100, image.width
- assert_equal 67, image.height
- end
-end
diff --git a/activestorage/test/database/setup.rb b/activestorage/test/database/setup.rb
index 87564499e6..daeeb5695b 100644
--- a/activestorage/test/database/setup.rb
+++ b/activestorage/test/database/setup.rb
@@ -3,5 +3,5 @@
require_relative "create_users_migration"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
-ActiveRecord::Migrator.migrate File.expand_path("../../../db/migrate", __FILE__)
+ActiveRecord::Base.connection.migration_context.migrate
ActiveStorageCreateUsers.migrate(:up)
diff --git a/activestorage/test/dummy/bin/bundle b/activestorage/test/dummy/bin/bundle
index 277e128251..5015ba6f8b 100755
--- a/activestorage/test/dummy/bin/bundle
+++ b/activestorage/test/dummy/bin/bundle
@@ -1,5 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
-ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
load Gem.bin_path("bundler", "bundle")
diff --git a/activestorage/test/dummy/config/application.rb b/activestorage/test/dummy/config/application.rb
index 7ee6625bb5..bd14ac0b1a 100644
--- a/activestorage/test/dummy/config/application.rb
+++ b/activestorage/test/dummy/config/application.rb
@@ -10,16 +10,12 @@ require "action_controller/railtie"
require "action_view/railtie"
require "sprockets/railtie"
require "active_storage/engine"
-#require "action_mailer/railtie"
-#require "rails/test_unit/railtie"
-#require "action_cable/engine"
-
Bundler.require(*Rails.groups)
module Dummy
class Application < Rails::Application
- config.load_defaults 5.1
+ config.load_defaults 5.2
config.active_storage.service = :local
end
diff --git a/activestorage/test/dummy/config/environments/test.rb b/activestorage/test/dummy/config/environments/test.rb
index ce0889e8ae..74a802d98c 100644
--- a/activestorage/test/dummy/config/environments/test.rb
+++ b/activestorage/test/dummy/config/environments/test.rb
@@ -30,6 +30,9 @@ Rails.application.configure do
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
end
diff --git a/activestorage/test/fixtures/files/favicon.ico b/activestorage/test/fixtures/files/favicon.ico
new file mode 100644
index 0000000000..87192a8a07
--- /dev/null
+++ b/activestorage/test/fixtures/files/favicon.ico
Binary files differ
diff --git a/activestorage/test/fixtures/files/icon.psd b/activestorage/test/fixtures/files/icon.psd
new file mode 100644
index 0000000000..631fceeaab
--- /dev/null
+++ b/activestorage/test/fixtures/files/icon.psd
Binary files differ
diff --git a/activestorage/test/fixtures/files/icon.svg b/activestorage/test/fixtures/files/icon.svg
new file mode 100644
index 0000000000..6cfb0e241e
--- /dev/null
+++ b/activestorage/test/fixtures/files/icon.svg
@@ -0,0 +1,13 @@
+<!-- The XML declaration is intentionally omitted. -->
+<svg width="792px" height="584px" viewBox="0 0 792 584" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="M9.51802657,28.724593 C9.51802657,18.2155955 18.1343454,9.60822622 28.6542694,9.60822622 L763.245541,9.60822622 C773.765465,9.60822622 782.381784,18.2155955 782.381784,28.724593 L782.381784,584 L792,584 L792,28.724593 C792,12.911054 779.075522,0 763.245541,0 L28.7544592,0 C12.9244782,0 0,12.911054 0,28.724593 L0,584 L9.61821632,584 C9.51802657,584 9.51802657,28.724593 9.51802657,28.724593 L9.51802657,28.724593 Z" id="Shape" opacity="0.3" fill="#CCCCCC"></path>
+ <circle id="Oval" fill="#FFCC33" cx="119.1" cy="147.2" r="33"></circle>
+ <circle id="Oval" fill="#3399FF" cx="119.1" cy="281.1" r="33"></circle>
+ <circle id="Oval" fill="#FF3333" cx="676.1" cy="376.8" r="33"></circle>
+ <circle id="Oval" fill="#FFCC33" cx="119.1" cy="477.2" r="33"></circle>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="130.5" width="442.1" height="33.5"></rect>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="183.1" width="442.1" height="33.5"></rect>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="265.8" width="442.1" height="33.5"></rect>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="363.4" width="442.1" height="33.5"></rect>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="465.3" width="442.1" height="33.5"></rect>
+</svg>
diff --git a/activestorage/test/fixtures/files/image.gif b/activestorage/test/fixtures/files/image.gif
new file mode 100644
index 0000000000..90c05f671c
--- /dev/null
+++ b/activestorage/test/fixtures/files/image.gif
Binary files differ
diff --git a/activestorage/test/fixtures/files/racecar_rotated.jpg b/activestorage/test/fixtures/files/racecar_rotated.jpg
new file mode 100644
index 0000000000..89e6d54f98
--- /dev/null
+++ b/activestorage/test/fixtures/files/racecar_rotated.jpg
Binary files differ
diff --git a/activestorage/test/fixtures/files/report.pdf b/activestorage/test/fixtures/files/report.pdf
new file mode 100644
index 0000000000..cccb9b5d64
--- /dev/null
+++ b/activestorage/test/fixtures/files/report.pdf
Binary files differ
diff --git a/activestorage/test/fixtures/files/rotated_video.mp4 b/activestorage/test/fixtures/files/rotated_video.mp4
new file mode 100644
index 0000000000..4c7a4e9e57
--- /dev/null
+++ b/activestorage/test/fixtures/files/rotated_video.mp4
Binary files differ
diff --git a/activestorage/test/fixtures/files/video.mp4 b/activestorage/test/fixtures/files/video.mp4
new file mode 100644
index 0000000000..8fb1c5b24d
--- /dev/null
+++ b/activestorage/test/fixtures/files/video.mp4
Binary files differ
diff --git a/activestorage/test/fixtures/files/video_with_rectangular_samples.mp4 b/activestorage/test/fixtures/files/video_with_rectangular_samples.mp4
new file mode 100644
index 0000000000..12b04afc87
--- /dev/null
+++ b/activestorage/test/fixtures/files/video_with_rectangular_samples.mp4
Binary files differ
diff --git a/activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4 b/activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4
new file mode 100644
index 0000000000..eb354e756f
--- /dev/null
+++ b/activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4
Binary files differ
diff --git a/activestorage/test/fixtures/files/video_without_video_stream.mp4 b/activestorage/test/fixtures/files/video_without_video_stream.mp4
new file mode 100644
index 0000000000..e6a55f868b
--- /dev/null
+++ b/activestorage/test/fixtures/files/video_without_video_stream.mp4
Binary files differ
diff --git a/activestorage/test/models/attachments_test.rb b/activestorage/test/models/attachments_test.rb
index ac346c0087..ce83ec27d2 100644
--- a/activestorage/test/models/attachments_test.rb
+++ b/activestorage/test/models/attachments_test.rb
@@ -15,7 +15,7 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert_equal "funky.jpg", @user.avatar.filename.to_s
end
- test "attach existing sgid blob" do
+ test "attach existing blob from a signed ID" do
@user.avatar.attach create_blob(filename: "funky.jpg").signed_id
assert_equal "funky.jpg", @user.avatar.filename.to_s
end
@@ -27,7 +27,7 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
test "attach new blob from an UploadedFile" do
file = file_fixture "racecar.jpg"
- @user.avatar.attach Rack::Test::UploadedFile.new file
+ @user.avatar.attach Rack::Test::UploadedFile.new file.to_s
assert_equal "racecar.jpg", @user.avatar.filename.to_s
end
@@ -56,6 +56,78 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert ActiveStorage::Blob.service.exist?(@user.avatar.key)
end
+ test "replace attached blob with itself" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+
+ assert_no_changes -> { @user.reload.avatar.blob } do
+ assert_no_changes -> { @user.reload.avatar.attachment } do
+ assert_no_enqueued_jobs do
+ @user.avatar.attach @user.avatar.blob
+ end
+ end
+ end
+ end
+
+ test "replaced attached blob with itself by signed ID" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+
+ assert_no_changes -> { @user.reload.avatar.blob } do
+ assert_no_changes -> { @user.reload.avatar.attachment } do
+ assert_no_enqueued_jobs do
+ @user.avatar.attach @user.avatar.blob.signed_id
+ end
+ end
+ end
+ end
+
+ test "replace independent attached blob" do
+ @user.cover_photo.attach create_blob(filename: "funky.jpg")
+
+ perform_enqueued_jobs do
+ assert_difference -> { ActiveStorage::Blob.count }, +1 do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ @user.cover_photo.attach create_blob(filename: "town.jpg")
+ end
+ end
+ end
+
+ assert_equal "town.jpg", @user.cover_photo.filename.to_s
+ end
+
+ test "attach blob to new record" do
+ user = User.new(name: "Jason")
+
+ assert_no_changes -> { user.new_record? } do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ user.avatar.attach create_blob(filename: "funky.jpg")
+ end
+ end
+
+ assert_predicate user.avatar, :attached?
+ assert_equal "funky.jpg", user.avatar.filename.to_s
+
+ assert_difference -> { ActiveStorage::Attachment.count }, +1 do
+ user.save!
+ end
+
+ assert_predicate user.reload.avatar, :attached?
+ assert_equal "funky.jpg", user.avatar.filename.to_s
+ end
+
+ test "build new record with attached blob" do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ @user = User.new(name: "Jason", avatar: { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" })
+ end
+
+ assert_predicate @user, :new_record?
+ assert_predicate @user.avatar, :attached?
+ assert_equal "town.jpg", @user.avatar.filename.to_s
+
+ @user.save!
+ assert_predicate @user.reload.avatar, :attached?
+ assert_equal "town.jpg", @user.avatar.filename.to_s
+ end
+
test "access underlying associations of new blob" do
@user.avatar.attach create_blob(filename: "funky.jpg")
assert_equal @user, @user.avatar_attachment.record
@@ -63,12 +135,90 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert_equal "funky.jpg", @user.avatar_attachment.blob.filename.to_s
end
+ test "identify newly-attached, directly-uploaded blob" do
+ blob = directly_upload_file_blob(content_type: "application/octet-stream")
+
+ @user.avatar.attach(blob)
+
+ assert_equal "image/jpeg", @user.avatar.reload.content_type
+ assert_predicate @user.avatar, :identified?
+ end
+
+ test "identify and analyze newly-attached, directly-uploaded blob" do
+ blob = directly_upload_file_blob(content_type: "application/octet-stream")
+
+ perform_enqueued_jobs do
+ @user.avatar.attach blob
+ end
+
+ assert_equal true, @user.avatar.reload.metadata[:identified]
+ assert_equal 4104, @user.avatar.metadata[:width]
+ assert_equal 2736, @user.avatar.metadata[:height]
+ end
+
+ test "identify newly-attached blob only once" do
+ blob = create_file_blob
+ assert_predicate blob, :identified?
+
+ # The blob's backing file is a PNG image. Fudge its content type so we can tell if it's identified when we attach it.
+ blob.update! content_type: "application/octet-stream"
+
+ @user.avatar.attach blob
+ assert_equal "application/octet-stream", blob.content_type
+ end
+
+ test "analyze newly-attached blob" do
+ perform_enqueued_jobs do
+ @user.avatar.attach create_file_blob
+ end
+
+ assert_equal 4104, @user.avatar.reload.metadata[:width]
+ assert_equal 2736, @user.avatar.metadata[:height]
+ end
+
+ test "analyze attached blob only once" do
+ blob = create_file_blob
+
+ perform_enqueued_jobs do
+ @user.avatar.attach blob
+ end
+
+ assert_predicate blob.reload, :analyzed?
+
+ @user.avatar.detach
+
+ assert_no_enqueued_jobs do
+ @user.reload.avatar.attach blob
+ end
+ end
+
+ test "preserve existing metadata when analyzing a newly-attached blob" do
+ blob = create_file_blob(metadata: { foo: "bar" })
+
+ perform_enqueued_jobs do
+ @user.avatar.attach blob
+ end
+
+ assert_equal "bar", blob.reload.metadata[:foo]
+ end
+
+ test "detach blob" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+ avatar_blob_id = @user.avatar.blob.id
+ avatar_key = @user.avatar.key
+
+ @user.avatar.detach
+ assert_not_predicate @user.avatar, :attached?
+ assert ActiveStorage::Blob.exists?(avatar_blob_id)
+ assert ActiveStorage::Blob.service.exist?(avatar_key)
+ end
+
test "purge attached blob" do
@user.avatar.attach create_blob(filename: "funky.jpg")
avatar_key = @user.avatar.key
@user.avatar.purge
- assert_not @user.avatar.attached?
+ assert_not_predicate @user.avatar, :attached?
assert_not ActiveStorage::Blob.service.exist?(avatar_key)
end
@@ -77,13 +227,33 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
avatar_key = @user.avatar.key
perform_enqueued_jobs do
- @user.destroy
+ @user.reload.destroy
assert_nil ActiveStorage::Blob.find_by(key: avatar_key)
assert_not ActiveStorage::Blob.service.exist?(avatar_key)
end
end
+ test "delete attachment for independent blob when record is destroyed" do
+ @user.cover_photo.attach create_blob(filename: "funky.jpg")
+
+ @user.destroy
+ assert_not ActiveStorage::Attachment.exists?(record: @user, name: "cover_photo")
+ end
+
+ test "find with attached blob" do
+ records = %w[alice bob].map do |name|
+ User.create!(name: name).tap do |user|
+ user.avatar.attach create_blob(filename: "#{name}.jpg")
+ end
+ end
+
+ users = User.where(id: records.map(&:id)).with_attached_avatar.all
+
+ assert_equal "alice.jpg", users.first.avatar.filename.to_s
+ assert_equal "bob.jpg", users.second.avatar.filename.to_s
+ end
+
test "attach existing blobs" do
@user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg")
@@ -101,6 +271,48 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert_equal "country.jpg", @user.highlights.second.filename.to_s
end
+ test "attach blobs to new record" do
+ user = User.new(name: "Jason")
+
+ assert_no_changes -> { user.new_record? } do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ user.highlights.attach(
+ { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" },
+ { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" })
+ end
+ end
+
+ assert_predicate user.highlights, :attached?
+ assert_equal "town.jpg", user.highlights.first.filename.to_s
+ assert_equal "country.jpg", user.highlights.second.filename.to_s
+
+ assert_difference -> { ActiveStorage::Attachment.count }, +2 do
+ user.save!
+ end
+
+ assert_predicate user.reload.highlights, :attached?
+ assert_equal "town.jpg", user.highlights.first.filename.to_s
+ assert_equal "country.jpg", user.highlights.second.filename.to_s
+ end
+
+ test "build new record with attached blobs" do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ @user = User.new(name: "Jason", highlights: [
+ { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" },
+ { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" }])
+ end
+
+ assert_predicate @user, :new_record?
+ assert_predicate @user.highlights, :attached?
+ assert_equal "town.jpg", @user.highlights.first.filename.to_s
+ assert_equal "country.jpg", @user.highlights.second.filename.to_s
+
+ @user.save!
+ assert_predicate @user.reload.highlights, :attached?
+ assert_equal "town.jpg", @user.highlights.first.filename.to_s
+ assert_equal "country.jpg", @user.highlights.second.filename.to_s
+ end
+
test "find attached blobs" do
@user.highlights.attach(
{ io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" },
@@ -122,13 +334,75 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert_equal "town.jpg", @user.highlights_attachments.first.blob.filename.to_s
end
+ test "analyze newly-attached blobs" do
+ perform_enqueued_jobs do
+ @user.highlights.attach(
+ create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg"),
+ create_file_blob(filename: "video.mp4", content_type: "video/mp4"))
+ end
+
+ assert_equal 4104, @user.highlights.first.metadata[:width]
+ assert_equal 2736, @user.highlights.first.metadata[:height]
+
+ assert_equal 640, @user.highlights.second.metadata[:width]
+ assert_equal 480, @user.highlights.second.metadata[:height]
+ end
+
+ test "analyze attached blobs only once" do
+ blobs = [
+ create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg"),
+ create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ ]
+
+ perform_enqueued_jobs do
+ @user.highlights.attach(blobs)
+ end
+
+ assert blobs.each(&:reload).all?(&:analyzed?)
+
+ @user.highlights.attachments.destroy_all
+
+ assert_no_enqueued_jobs do
+ @user.highlights.attach(blobs)
+ end
+ end
+
+ test "preserve existing metadata when analyzing newly-attached blobs" do
+ blobs = [
+ create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg", metadata: { foo: "bar" }),
+ create_file_blob(filename: "video.mp4", content_type: "video/mp4", metadata: { foo: "bar" })
+ ]
+
+ perform_enqueued_jobs do
+ @user.highlights.attach(blobs)
+ end
+
+ blobs.each do |blob|
+ assert_equal "bar", blob.reload.metadata[:foo]
+ end
+ end
+
+ test "detach blobs" do
+ @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg")
+ highlight_blob_ids = @user.highlights.collect { |highlight| highlight.blob.id }
+ highlight_keys = @user.highlights.collect(&:key)
+
+ @user.highlights.detach
+ assert_not_predicate @user.highlights, :attached?
+
+ assert ActiveStorage::Blob.exists?(highlight_blob_ids.first)
+ assert ActiveStorage::Blob.exists?(highlight_blob_ids.second)
+
+ assert ActiveStorage::Blob.service.exist?(highlight_keys.first)
+ assert ActiveStorage::Blob.service.exist?(highlight_keys.second)
+ end
test "purge attached blobs" do
@user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg")
highlight_keys = @user.highlights.collect(&:key)
@user.highlights.purge
- assert_not @user.highlights.attached?
+ assert_not_predicate @user.highlights, :attached?
assert_not ActiveStorage::Blob.service.exist?(highlight_keys.first)
assert_not ActiveStorage::Blob.service.exist?(highlight_keys.second)
end
@@ -138,7 +412,7 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
highlight_keys = @user.highlights.collect(&:key)
perform_enqueued_jobs do
- @user.destroy
+ @user.reload.destroy
assert_nil ActiveStorage::Blob.find_by(key: highlight_keys.first)
assert_not ActiveStorage::Blob.service.exist?(highlight_keys.first)
@@ -147,4 +421,39 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert_not ActiveStorage::Blob.service.exist?(highlight_keys.second)
end
end
+
+ test "delete attachments for independent blobs when the record is destroyed" do
+ @user.vlogs.attach create_blob(filename: "funky.mp4"), create_blob(filename: "wonky.mp4")
+
+ @user.destroy
+ assert_not ActiveStorage::Attachment.exists?(record: @user, name: "vlogs")
+ end
+
+ test "selectively purge one attached blob of many" do
+ first_blob = create_blob(filename: "funky.jpg")
+ second_blob = create_blob(filename: "wonky.jpg")
+ attachments = @user.highlights.attach(first_blob, second_blob)
+
+ assert_difference -> { ActiveStorage::Blob.count }, -1 do
+ @user.highlights.where(id: attachments.first.id).purge
+ end
+
+ assert_not ActiveStorage::Blob.exists?(key: first_blob.key)
+ assert ActiveStorage::Blob.exists?(key: second_blob.key)
+ end
+
+ test "selectively purge one attached blob of many later" do
+ first_blob = create_blob(filename: "funky.jpg")
+ second_blob = create_blob(filename: "wonky.jpg")
+ attachments = @user.highlights.attach(first_blob, second_blob)
+
+ perform_enqueued_jobs do
+ assert_difference -> { ActiveStorage::Blob.count }, -1 do
+ @user.highlights.where(id: attachments.first.id).purge_later
+ end
+ end
+
+ assert_not ActiveStorage::Blob.exists?(key: first_blob.key)
+ assert ActiveStorage::Blob.exists?(key: second_blob.key)
+ end
end
diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb
index 6e815997ba..fead17d33a 100644
--- a/activestorage/test/models/blob_test.rb
+++ b/activestorage/test/models/blob_test.rb
@@ -2,8 +2,28 @@
require "test_helper"
require "database/setup"
+require "active_support/testing/method_call_assertions"
class ActiveStorage::BlobTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
+ test ".unattached scope returns not attached blobs" do
+ class UserWithHasOneAttachedDependentFalse < User
+ has_one_attached :avatar, dependent: false
+ end
+
+ ActiveStorage::Blob.delete_all
+ blob_1 = create_blob filename: "funky.jpg"
+ blob_2 = create_blob filename: "town.jpg"
+
+ user = UserWithHasOneAttachedDependentFalse.create!
+ user.avatar.attach blob_1
+
+ assert_equal [blob_2], ActiveStorage::Blob.unattached
+ user.destroy
+ assert_equal [blob_1, blob_2].map(&:id).sort, ActiveStorage::Blob.unattached.pluck(:id).sort
+ end
+
test "create after upload sets byte size and checksum" do
data = "Hello world!"
blob = create_blob data: data
@@ -13,10 +33,32 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
assert_equal Digest::MD5.base64digest(data), blob.checksum
end
+ test "create after upload extracts content type from data" do
+ blob = create_file_blob content_type: "application/octet-stream"
+ assert_equal "image/jpeg", blob.content_type
+ end
+
+ test "create after upload extracts content type from filename" do
+ blob = create_blob content_type: "application/octet-stream"
+ assert_equal "text/plain", blob.content_type
+ end
+
+ test "image?" do
+ blob = create_file_blob filename: "racecar.jpg"
+ assert_predicate blob, :image?
+ assert_not_predicate blob, :audio?
+ end
+
+ test "video?" do
+ blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ assert_predicate blob, :video?
+ assert_not_predicate blob, :audio?
+ end
+
test "text?" do
blob = create_blob data: "Hello world!"
- assert blob.text?
- assert_not blob.audio?
+ assert_predicate blob, :text?
+ assert_not_predicate blob, :audio?
end
test "download yields chunks" do
@@ -41,16 +83,63 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
end
end
- test "purge removes from external service" do
+ test "urls force attachment as content disposition for content types served as binary" do
+ blob = create_blob(content_type: "text/html")
+
+ freeze_time do
+ assert_equal expected_url_for(blob, disposition: :attachment), blob.service_url
+ assert_equal expected_url_for(blob, disposition: :attachment), blob.service_url(disposition: :inline)
+ end
+ end
+
+ test "urls allow for custom filename" do
+ blob = create_blob(filename: "original.txt")
+ new_filename = ActiveStorage::Filename.new("new.txt")
+
+ freeze_time do
+ assert_equal expected_url_for(blob), blob.service_url
+ assert_equal expected_url_for(blob, filename: new_filename), blob.service_url(filename: new_filename)
+ assert_equal expected_url_for(blob, filename: new_filename), blob.service_url(filename: "new.txt")
+ assert_equal expected_url_for(blob, filename: blob.filename), blob.service_url(filename: nil)
+ end
+ end
+
+ test "urls allow for custom options" do
+ blob = create_blob(filename: "original.txt")
+
+ options = [
+ blob.key,
+ expires_in: blob.service.url_expires_in,
+ disposition: :inline,
+ content_type: blob.content_type,
+ filename: blob.filename,
+ thumb_size: "300x300",
+ thumb_mode: "crop"
+ ]
+ assert_called_with(blob.service, :url, options) do
+ blob.service_url(thumb_size: "300x300", thumb_mode: "crop")
+ end
+ end
+
+ test "purge deletes file from external service" do
blob = create_blob
blob.purge
assert_not ActiveStorage::Blob.service.exist?(blob.key)
end
+ test "purge deletes variants from external service" do
+ blob = create_file_blob
+ variant = blob.variant(resize: "100>").processed
+
+ blob.purge
+ assert_not ActiveStorage::Blob.service.exist?(variant.key)
+ end
+
private
- def expected_url_for(blob, disposition: :inline)
- query_string = { content_type: blob.content_type, disposition: "#{disposition}; #{blob.filename.parameters}" }.to_param
- "/rails/active_storage/disk/#{ActiveStorage.verifier.generate(blob.key, expires_in: 5.minutes, purpose: :blob_key)}/#{blob.filename}?#{query_string}"
+ def expected_url_for(blob, disposition: :inline, filename: nil)
+ filename ||= blob.filename
+ query_string = { content_type: blob.content_type, disposition: "#{disposition}; #{filename.parameters}" }.to_param
+ "https://example.com/rails/active_storage/disk/#{ActiveStorage.verifier.generate(blob.key, expires_in: 5.minutes, purpose: :blob_key)}/#{filename}?#{query_string}"
end
end
diff --git a/activestorage/test/models/presence_validation_test.rb b/activestorage/test/models/presence_validation_test.rb
new file mode 100644
index 0000000000..aa804506dd
--- /dev/null
+++ b/activestorage/test/models/presence_validation_test.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::PresenceValidationTest < ActiveSupport::TestCase
+ class Admin < User; end
+
+ teardown do
+ Admin.clear_validators!
+ end
+
+ test "validates_presence_of has_one_attached" do
+ Admin.validates_presence_of :avatar
+ a = Admin.new
+ assert_predicate a, :invalid?
+
+ a.avatar.attach create_blob(filename: "funky.jpg")
+ assert_predicate a, :valid?
+ end
+
+ test "validates_presence_of has_many_attached" do
+ Admin.validates_presence_of :highlights
+ a = Admin.new
+ assert_predicate a, :invalid?
+
+ a.highlights.attach create_blob(filename: "funky.jpg")
+ assert_predicate a, :valid?
+ end
+end
diff --git a/activestorage/test/models/preview_test.rb b/activestorage/test/models/preview_test.rb
new file mode 100644
index 0000000000..55fdc228c8
--- /dev/null
+++ b/activestorage/test/models/preview_test.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::PreviewTest < ActiveSupport::TestCase
+ test "previewing a PDF" do
+ blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ preview = blob.preview(resize: "640x280").processed
+
+ assert_predicate preview.image, :attached?
+ assert_equal "report.png", preview.image.filename.to_s
+ assert_equal "image/png", preview.image.content_type
+
+ image = read_image(preview.image)
+ assert_equal 612, image.width
+ assert_equal 792, image.height
+ end
+
+ test "previewing an MP4 video" do
+ blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ preview = blob.preview(resize: "640x280").processed
+
+ assert_predicate preview.image, :attached?
+ assert_equal "video.png", preview.image.filename.to_s
+ assert_equal "image/png", preview.image.content_type
+
+ image = read_image(preview.image)
+ assert_equal 640, image.width
+ assert_equal 480, image.height
+ end
+
+ test "previewing an unpreviewable blob" do
+ blob = create_file_blob
+
+ assert_raises ActiveStorage::UnpreviewableError do
+ blob.preview resize: "640x280"
+ end
+ end
+end
diff --git a/activestorage/test/models/representation_test.rb b/activestorage/test/models/representation_test.rb
new file mode 100644
index 0000000000..2a06b31c77
--- /dev/null
+++ b/activestorage/test/models/representation_test.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::RepresentationTest < ActiveSupport::TestCase
+ test "representing an image" do
+ blob = create_file_blob
+ representation = blob.representation(resize: "100x100").processed
+
+ image = read_image(representation.image)
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ end
+
+ test "representing a PDF" do
+ blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ representation = blob.representation(resize: "640x280").processed
+
+ image = read_image(representation.image)
+ assert_equal 612, image.width
+ assert_equal 792, image.height
+ end
+
+ test "representing an MP4 video" do
+ blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ representation = blob.representation(resize: "640x280").processed
+
+ image = read_image(representation.image)
+ assert_equal 640, image.width
+ assert_equal 480, image.height
+ end
+
+ test "representing an unrepresentable blob" do
+ blob = create_blob
+
+ assert_raises ActiveStorage::UnrepresentableError do
+ blob.representation resize: "100x100"
+ end
+ end
+end
diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb
index ca112ab907..e74bbc9ab4 100644
--- a/activestorage/test/models/variant_test.rb
+++ b/activestorage/test/models/variant_test.rb
@@ -4,26 +4,113 @@ require "test_helper"
require "database/setup"
class ActiveStorage::VariantTest < ActiveSupport::TestCase
- setup do
- @blob = create_image_blob filename: "racecar.jpg"
- end
-
- test "resized variation" do
- variant = @blob.variant(resize: "100x100").processed
+ test "resized variation of JPEG blob" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(resize: "100x100").processed
assert_match(/racecar\.jpg/, variant.service_url)
- image = read_image_variant(variant)
+ image = read_image(variant)
assert_equal 100, image.width
assert_equal 67, image.height
end
- test "resized and monochrome variation" do
- variant = @blob.variant(resize: "100x100", monochrome: true).processed
+ test "resized and monochrome variation of JPEG blob" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(resize: "100x100", monochrome: true).processed
assert_match(/racecar\.jpg/, variant.service_url)
- image = read_image_variant(variant)
+ image = read_image(variant)
assert_equal 100, image.width
assert_equal 67, image.height
assert_match(/Gray/, image.colorspace)
end
+
+ test "center-weighted crop of JPEG blob using :combine_options" do
+ begin
+ ActiveStorage.variant_processor = nil
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = ActiveSupport::Deprecation.silence do
+ blob.variant(combine_options: {
+ gravity: "center",
+ resize: "100x100^",
+ crop: "100x100+0+0",
+ }).processed
+ end
+ assert_match(/racecar\.jpg/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 100, image.height
+ ensure
+ ActiveStorage.variant_processor = :mini_magick
+ end
+ end
+
+ test "center-weighted crop of JPEG blob using :resize_to_fill" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(resize_to_fill: [100, 100]).processed
+ assert_match(/racecar\.jpg/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 100, image.height
+ end
+
+ test "resized variation of PSD blob" do
+ blob = create_file_blob(filename: "icon.psd", content_type: "image/vnd.adobe.photoshop")
+ variant = blob.variant(resize: "20x20").processed
+ assert_match(/icon\.png/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal "PNG", image.type
+ assert_equal 20, image.width
+ assert_equal 20, image.height
+ end
+
+ test "resized variation of ICO blob" do
+ blob = create_file_blob(filename: "favicon.ico", content_type: "image/vnd.microsoft.icon")
+ variant = blob.variant(resize: "20x20").processed
+ assert_match(/icon\.png/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal "PNG", image.type
+ assert_equal 20, image.width
+ assert_equal 20, image.height
+ end
+
+ test "optimized variation of GIF blob" do
+ blob = create_file_blob(filename: "image.gif", content_type: "image/gif")
+
+ assert_nothing_raised do
+ blob.variant(layers: "Optimize").processed
+ end
+ end
+
+ test "variation of invariable blob" do
+ assert_raises ActiveStorage::InvariableError do
+ create_file_blob(filename: "report.pdf", content_type: "application/pdf").variant(resize: "100x100")
+ end
+ end
+
+ test "service_url doesn't grow in length despite long variant options" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(font: "a" * 10_000).processed
+ assert_operator variant.service_url.length, :<, 525
+ end
+
+ test "works for vips processor" do
+ begin
+ ActiveStorage.variant_processor = :vips
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(thumbnail_image: 100).processed
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ rescue LoadError
+ # libvips not installed
+ ensure
+ ActiveStorage.variant_processor = :mini_magick
+ end
+ end
end
diff --git a/activestorage/test/previewer/mupdf_previewer_test.rb b/activestorage/test/previewer/mupdf_previewer_test.rb
new file mode 100644
index 0000000000..6c2db6fcbf
--- /dev/null
+++ b/activestorage/test/previewer/mupdf_previewer_test.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/previewer/mupdf_previewer"
+
+class ActiveStorage::Previewer::MuPDFPreviewerTest < ActiveSupport::TestCase
+ setup do
+ @blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ end
+
+ test "previewing a PDF document" do
+ ActiveStorage::Previewer::MuPDFPreviewer.new(@blob).preview do |attachable|
+ assert_equal "image/png", attachable[:content_type]
+ assert_equal "report.png", attachable[:filename]
+
+ image = MiniMagick::Image.read(attachable[:io])
+ assert_equal 612, image.width
+ assert_equal 792, image.height
+ end
+ end
+end
diff --git a/activestorage/test/previewer/poppler_pdf_previewer_test.rb b/activestorage/test/previewer/poppler_pdf_previewer_test.rb
new file mode 100644
index 0000000000..2b41c8b642
--- /dev/null
+++ b/activestorage/test/previewer/poppler_pdf_previewer_test.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/previewer/poppler_pdf_previewer"
+
+class ActiveStorage::Previewer::PopplerPDFPreviewerTest < ActiveSupport::TestCase
+ setup do
+ @blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ end
+
+ test "previewing a PDF document" do
+ ActiveStorage::Previewer::PopplerPDFPreviewer.new(@blob).preview do |attachable|
+ assert_equal "image/png", attachable[:content_type]
+ assert_equal "report.png", attachable[:filename]
+
+ image = MiniMagick::Image.read(attachable[:io])
+ assert_equal 612, image.width
+ assert_equal 792, image.height
+ end
+ end
+end
diff --git a/activestorage/test/previewer/video_previewer_test.rb b/activestorage/test/previewer/video_previewer_test.rb
new file mode 100644
index 0000000000..dba9b0d7e2
--- /dev/null
+++ b/activestorage/test/previewer/video_previewer_test.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/previewer/video_previewer"
+
+class ActiveStorage::Previewer::VideoPreviewerTest < ActiveSupport::TestCase
+ setup do
+ @blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ end
+
+ test "previewing an MP4 video" do
+ ActiveStorage::Previewer::VideoPreviewer.new(@blob).preview do |attachable|
+ assert_equal "image/png", attachable[:content_type]
+ assert_equal "video.png", attachable[:filename]
+
+ image = MiniMagick::Image.read(attachable[:io])
+ assert_equal 640, image.width
+ assert_equal 480, image.height
+ end
+ end
+end
diff --git a/activestorage/test/service/azure_storage_service_test.rb b/activestorage/test/service/azure_storage_service_test.rb
index 4729bdfbc5..be31bbe858 100644
--- a/activestorage/test/service/azure_storage_service_test.rb
+++ b/activestorage/test/service/azure_storage_service_test.rb
@@ -11,9 +11,9 @@ if SERVICE_CONFIGURATIONS[:azure]
test "signed URL generation" do
url = @service.url(FIXTURE_KEY, expires_in: 5.minutes,
- disposition: "inline; filename=\"avatar.png\"", filename: "avatar.png", content_type: "image/png")
+ disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")
- assert_match(/(\S+)&rscd=inline%3B\+filename%3D%22avatar.png%22&rsct=image%2Fpng/, url)
+ assert_match(/(\S+)&rscd=inline%3B\+filename%3D%22avatar\.png%22%3B\+filename\*%3DUTF-8%27%27avatar\.png&rsct=image%2Fpng/, url)
assert_match SERVICE_CONFIGURATIONS[:azure][:container], url
end
end
diff --git a/activestorage/test/service/configurations.example.yml b/activestorage/test/service/configurations.example.yml
index 56ed37be5d..a63aa33302 100644
--- a/activestorage/test/service/configurations.example.yml
+++ b/activestorage/test/service/configurations.example.yml
@@ -7,7 +7,7 @@
#
# gcs:
# service: GCS
-# keyfile: {
+# credentials: {
# type: "service_account",
# project_id: "",
# private_key_id: "",
@@ -24,7 +24,6 @@
#
# azure:
# service: AzureStorage
-# path: ""
# storage_account_name: ""
# storage_access_key: ""
# container: ""
diff --git a/activestorage/test/service/configurations.yml.enc b/activestorage/test/service/configurations.yml.enc
index df11aac161..648924a562 100644
--- a/activestorage/test/service/configurations.yml.enc
+++ b/activestorage/test/service/configurations.yml.enc
Binary files differ
diff --git a/activestorage/test/service/configurator_test.rb b/activestorage/test/service/configurator_test.rb
index a2fd035e02..1c9c5c3aa0 100644
--- a/activestorage/test/service/configurator_test.rb
+++ b/activestorage/test/service/configurator_test.rb
@@ -6,6 +6,7 @@ class ActiveStorage::Service::ConfiguratorTest < ActiveSupport::TestCase
test "builds correct service instance based on service name" do
service = ActiveStorage::Service::Configurator.build(:foo, foo: { service: "Disk", root: "path" })
assert_instance_of ActiveStorage::Service::DiskService, service
+ assert_equal "path", service.root
end
test "raises error when passing non-existent service name" do
diff --git a/activestorage/test/service/disk_service_test.rb b/activestorage/test/service/disk_service_test.rb
index e07d1d88bc..d7142de458 100644
--- a/activestorage/test/service/disk_service_test.rb
+++ b/activestorage/test/service/disk_service_test.rb
@@ -8,7 +8,7 @@ class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase
include ActiveStorage::Service::SharedServiceTests
test "url generation" do
- assert_match(/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/,
- @service.url(FIXTURE_KEY, expires_in: 5.minutes, disposition: "inline; filename=\"avatar.png\"", filename: "avatar.png", content_type: "image/png"))
+ assert_match(/^https:\/\/example.com\/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/,
+ @service.url(FIXTURE_KEY, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png"))
end
end
diff --git a/activestorage/test/service/gcs_service_test.rb b/activestorage/test/service/gcs_service_test.rb
index f664cee90b..fc2d9d0fa7 100644
--- a/activestorage/test/service/gcs_service_test.rb
+++ b/activestorage/test/service/gcs_service_test.rb
@@ -19,7 +19,7 @@ if SERVICE_CONFIGURATIONS[:gcs]
uri = URI.parse url
request = Net::HTTP::Put.new uri.request_uri
request.body = data
- request.add_field "Content-Type", "text/plain"
+ request.add_field "Content-Type", ""
request.add_field "Content-MD5", checksum
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request request
@@ -32,12 +32,21 @@ if SERVICE_CONFIGURATIONS[:gcs]
end
test "signed URL generation" do
- freeze_time do
- url = SERVICE.bucket.signed_url(FIXTURE_KEY, expires: 120) +
- "&response-content-disposition=inline%3B+filename%3D%22test.txt%22" +
- "&response-content-type=text%2Fplain"
+ assert_match(/storage\.googleapis\.com\/.*response-content-disposition=inline.*test\.txt.*response-content-type=text%2Fplain/,
+ @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain"))
+ end
+
+ test "signed URL response headers" do
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest(data))
- assert_equal url, @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: "inline; filename=\"test.txt\"", filename: "test.txt", content_type: "text/plain")
+ url = @service.url(key, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain")
+ response = Net::HTTP.get_response(URI(url))
+ assert_equal "text/plain", response.header["Content-Type"]
+ ensure
+ @service.delete key
end
end
end
diff --git a/activestorage/test/service/mirror_service_test.rb b/activestorage/test/service/mirror_service_test.rb
index 93e86eff70..87306644c5 100644
--- a/activestorage/test/service/mirror_service_test.rb
+++ b/activestorage/test/service/mirror_service_test.rb
@@ -10,8 +10,8 @@ class ActiveStorage::Service::MirrorServiceTest < ActiveSupport::TestCase
end.to_h
config = mirror_config.merge \
- mirror: { service: "Mirror", primary: "primary", mirrors: mirror_config.keys },
- primary: { service: "Disk", root: Dir.mktmpdir("active_storage_tests_primary") }
+ mirror: { service: "Mirror", primary: "primary", mirrors: mirror_config.keys },
+ primary: { service: "Disk", root: Dir.mktmpdir("active_storage_tests_primary") }
SERVICE = ActiveStorage::Service.configure :mirror, config
@@ -19,11 +19,16 @@ class ActiveStorage::Service::MirrorServiceTest < ActiveSupport::TestCase
test "uploading to all services" do
begin
- data = "Something else entirely!"
- key = upload(data, to: @service)
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ io = StringIO.new(data)
+ checksum = Digest::MD5.base64digest(data)
- assert_equal data, SERVICE.primary.download(key)
- SERVICE.mirrors.each do |mirror|
+ @service.upload key, io.tap(&:read), checksum: checksum
+ assert_predicate io, :eof?
+
+ assert_equal data, @service.primary.download(key)
+ @service.mirrors.each do |mirror|
assert_equal data, mirror.download(key)
end
ensure
@@ -32,14 +37,18 @@ class ActiveStorage::Service::MirrorServiceTest < ActiveSupport::TestCase
end
test "downloading from primary service" do
- data = "Something else entirely!"
- key = upload(data, to: SERVICE.primary)
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ checksum = Digest::MD5.base64digest(data)
+
+ @service.primary.upload key, StringIO.new(data), checksum: checksum
assert_equal data, @service.download(key)
end
test "deleting from all services" do
@service.delete FIXTURE_KEY
+
assert_not SERVICE.primary.exist?(FIXTURE_KEY)
SERVICE.mirrors.each do |mirror|
assert_not mirror.exist?(FIXTURE_KEY)
@@ -47,18 +56,11 @@ class ActiveStorage::Service::MirrorServiceTest < ActiveSupport::TestCase
end
test "URL generation in primary service" do
+ filename = ActiveStorage::Filename.new("test.txt")
+
freeze_time do
- assert_equal SERVICE.primary.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: "test.txt", content_type: "text/plain"),
- @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: "test.txt", content_type: "text/plain")
+ assert_equal @service.primary.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: filename, content_type: "text/plain"),
+ @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: filename, content_type: "text/plain")
end
end
-
- private
- def upload(data, to:)
- SecureRandom.base58(24).tap do |key|
- io = StringIO.new(data).tap(&:read)
- @service.upload key, io, checksum: Digest::MD5.base64digest(data)
- assert io.eof?
- end
- end
end
diff --git a/activestorage/test/service/s3_service_test.rb b/activestorage/test/service/s3_service_test.rb
index c07d6396b1..7833e51122 100644
--- a/activestorage/test/service/s3_service_test.rb
+++ b/activestorage/test/service/s3_service_test.rb
@@ -3,7 +3,7 @@
require "service/shared_service_tests"
require "net/http"
-if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].present?
+if SERVICE_CONFIGURATIONS[:s3]
class ActiveStorage::Service::S3ServiceTest < ActiveSupport::TestCase
SERVICE = ActiveStorage::Service.configure(:s3, SERVICE_CONFIGURATIONS)
@@ -33,9 +33,9 @@ if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].pr
test "signed URL generation" do
url = @service.url(FIXTURE_KEY, expires_in: 5.minutes,
- disposition: "inline; filename=\"avatar.png\"", filename: "avatar.png", content_type: "image/png")
+ disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")
- assert_match(/s3\.(\S+)?amazonaws.com.*response-content-disposition=inline.*avatar\.png.*response-content-type=image%2Fpng/, url)
+ assert_match(/s3(-[-a-z0-9]+)?\.(\S+)?amazonaws.com.*response-content-disposition=inline.*avatar\.png.*response-content-type=image%2Fpng/, url)
assert_match SERVICE_CONFIGURATIONS[:s3][:bucket], url
end
diff --git a/activestorage/test/service/shared_service_tests.rb b/activestorage/test/service/shared_service_tests.rb
index a9e1cb6ce9..24debe7f47 100644
--- a/activestorage/test/service/shared_service_tests.rb
+++ b/activestorage/test/service/shared_service_tests.rb
@@ -50,6 +50,21 @@ module ActiveStorage::Service::SharedServiceTests
assert_equal FIXTURE_DATA, @service.download(FIXTURE_KEY)
end
+ test "downloading in chunks" do
+ chunks = []
+
+ @service.download(FIXTURE_KEY) do |chunk|
+ chunks << chunk
+ end
+
+ assert_equal [ FIXTURE_DATA ], chunks
+ end
+
+ test "downloading partially" do
+ assert_equal "\x10\x00\x00", @service.download_chunk(FIXTURE_KEY, 19..21)
+ assert_equal "\x10\x00\x00", @service.download_chunk(FIXTURE_KEY, 19...22)
+ end
+
test "existing" do
assert @service.exist?(FIXTURE_KEY)
assert_not @service.exist?(FIXTURE_KEY + "nonsense")
@@ -65,5 +80,22 @@ module ActiveStorage::Service::SharedServiceTests
@service.delete SecureRandom.base58(24)
end
end
+
+ test "deleting by prefix" do
+ begin
+ @service.upload("a/a/a", StringIO.new(FIXTURE_DATA))
+ @service.upload("a/a/b", StringIO.new(FIXTURE_DATA))
+ @service.upload("a/b/a", StringIO.new(FIXTURE_DATA))
+
+ @service.delete_prefixed("a/a/")
+ assert_not @service.exist?("a/a/a")
+ assert_not @service.exist?("a/a/b")
+ assert @service.exist?("a/b/a")
+ ensure
+ @service.delete("a/a/a")
+ @service.delete("a/a/b")
+ @service.delete("a/b/a")
+ end
+ end
end
end
diff --git a/activestorage/test/template/image_tag_test.rb b/activestorage/test/template/image_tag_test.rb
index dedc58452e..f0b166c225 100644
--- a/activestorage/test/template/image_tag_test.rb
+++ b/activestorage/test/template/image_tag_test.rb
@@ -7,7 +7,7 @@ class ActiveStorage::ImageTagTest < ActionView::TestCase
tests ActionView::Helpers::AssetTagHelper
setup do
- @blob = create_image_blob filename: "racecar.jpg"
+ @blob = create_file_blob filename: "racecar.jpg"
end
test "blob" do
@@ -19,6 +19,12 @@ class ActiveStorage::ImageTagTest < ActionView::TestCase
assert_dom_equal %(<img src="#{polymorphic_url variant}" />), image_tag(variant)
end
+ test "preview" do
+ blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ preview = blob.preview(resize: "100x100")
+ assert_dom_equal %(<img src="#{polymorphic_url preview}" />), image_tag(preview)
+ end
+
test "attachment" do
attachment = ActiveStorage::Attachment.new(blob: @blob)
assert_dom_equal %(<img src="#{polymorphic_url attachment}" />), image_tag(attachment)
@@ -27,7 +33,7 @@ class ActiveStorage::ImageTagTest < ActionView::TestCase
test "error when attachment's empty" do
@user = User.create!(name: "DHH")
- assert_not @user.avatar.attached?
+ assert_not_predicate @user.avatar, :attached?
assert_raises(ArgumentError) { image_tag(@user.avatar) }
end
diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb
index 2a969fa326..499d955a2f 100644
--- a/activestorage/test/test_helper.rb
+++ b/activestorage/test/test_helper.rb
@@ -1,11 +1,13 @@
# frozen_string_literal: true
-require File.expand_path("../../test/dummy/config/environment.rb", __FILE__)
+ENV["RAILS_ENV"] ||= "test"
+require_relative "dummy/config/environment.rb"
require "bundler/setup"
require "active_support"
require "active_support/test_case"
require "active_support/testing/autorun"
+require "image_processing/mini_magick"
begin
require "byebug"
@@ -22,7 +24,7 @@ Minitest.backtrace_filter = Minitest::BacktraceFilter.new
require "yaml"
SERVICE_CONFIGURATIONS = begin
- erb = ERB.new(Pathname.new(File.expand_path("../service/configurations.yml", __FILE__)).read)
+ erb = ERB.new(Pathname.new(File.expand_path("service/configurations.yml", __dir__)).read)
configuration = YAML.load(erb.result) || {}
configuration.deep_symbolize_keys
rescue Errno::ENOENT
@@ -32,30 +34,50 @@ end
require "tmpdir"
ActiveStorage::Blob.service = ActiveStorage::Service::DiskService.new(root: Dir.mktmpdir("active_storage_tests"))
-ActiveStorage::Service.logger = ActiveSupport::Logger.new(nil)
+ActiveStorage.logger = ActiveSupport::Logger.new(nil)
ActiveStorage.verifier = ActiveSupport::MessageVerifier.new("Testing")
class ActiveSupport::TestCase
- self.file_fixture_path = File.expand_path("../fixtures/files", __FILE__)
+ self.file_fixture_path = File.expand_path("fixtures/files", __dir__)
+
+ setup do
+ ActiveStorage::Current.host = "https://example.com"
+ end
+
+ teardown do
+ ActiveStorage::Current.reset
+ end
private
def create_blob(data: "Hello world!", filename: "hello.txt", content_type: "text/plain")
ActiveStorage::Blob.create_after_upload! io: StringIO.new(data), filename: filename, content_type: content_type
end
- def create_image_blob(filename: "racecar.jpg", content_type: "image/jpeg")
- ActiveStorage::Blob.create_after_upload! \
- io: file_fixture(filename).open,
- filename: filename, content_type: content_type
+ def create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg", metadata: nil)
+ ActiveStorage::Blob.create_after_upload! io: file_fixture(filename).open, filename: filename, content_type: content_type, metadata: metadata
end
def create_blob_before_direct_upload(filename: "hello.txt", byte_size:, checksum:, content_type: "text/plain")
ActiveStorage::Blob.create_before_direct_upload! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type
end
- def read_image_variant(variant)
- MiniMagick::Image.open variant.service.send(:path_for, variant.key)
+ def directly_upload_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
+ file = file_fixture(filename)
+ byte_size = file.size
+ checksum = Digest::MD5.file(file).base64digest
+
+ create_blob_before_direct_upload(filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type).tap do |blob|
+ ActiveStorage::Blob.service.upload(blob.key, file.open)
+ end
+ end
+
+ def read_image(blob_or_variant)
+ MiniMagick::Image.open blob_or_variant.service.send(:path_for, blob_or_variant.key)
+ end
+
+ def extract_metadata_from(blob)
+ blob.tap(&:analyze).metadata
end
end
@@ -65,5 +87,8 @@ ActiveRecord::Base.send :include, GlobalID::Identification
class User < ActiveRecord::Base
has_one_attached :avatar
+ has_one_attached :cover_photo, dependent: false
+
has_many_attached :highlights
+ has_many_attached :vlogs, dependent: false
end
diff --git a/activestorage/webpack.config.js b/activestorage/webpack.config.js
deleted file mode 100644
index 92c4530e7f..0000000000
--- a/activestorage/webpack.config.js
+++ /dev/null
@@ -1,27 +0,0 @@
-const webpack = require("webpack")
-const path = require("path")
-
-module.exports = {
- entry: {
- "activestorage": path.resolve(__dirname, "app/javascript/activestorage/index.js"),
- },
-
- output: {
- filename: "[name].js",
- path: path.resolve(__dirname, "app/assets/javascripts"),
- library: "ActiveStorage",
- libraryTarget: "umd"
- },
-
- module: {
- rules: [
- {
- test: /\.js$/,
- exclude: /node_modules/,
- use: {
- loader: "babel-loader"
- }
- }
- ]
- }
-}
diff --git a/activestorage/yarn.lock b/activestorage/yarn.lock
index dd09577445..44eae3c5b1 100644
--- a/activestorage/yarn.lock
+++ b/activestorage/yarn.lock
@@ -2,15 +2,13 @@
# yarn lockfile v1
-abbrev@1:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
+"@types/estree@0.0.38":
+ version "0.0.38"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.38.tgz#c1be40aa933723c608820a99a373a16d215a1ca2"
-acorn-dynamic-import@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4"
- dependencies:
- acorn "^4.0.3"
+"@types/node@*":
+ version "9.6.6"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.6.tgz#439b91f9caf3983cad2eef1e11f6bedcbf9431d2"
acorn-jsx@^3.0.0:
version "3.0.1"
@@ -22,11 +20,7 @@ acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-acorn@^4.0.3:
- version "4.0.13"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
-
-acorn@^5.0.0, acorn@^5.0.1:
+acorn@^5.0.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75"
@@ -34,18 +28,14 @@ ajv-keywords@^1.0.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
-ajv-keywords@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0"
-
-ajv@^4.7.0, ajv@^4.9.1:
+ajv@^4.7.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
dependencies:
co "^4.6.0"
json-stable-stringify "^1.0.1"
-ajv@^5.1.5, ajv@^5.2.0:
+ajv@^5.2.0:
version "5.2.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
dependencies:
@@ -54,14 +44,6 @@ ajv@^5.1.5, ajv@^5.2.0:
json-schema-traverse "^0.3.0"
json-stable-stringify "^1.0.1"
-align-text@^0.1.1, align-text@^0.1.3:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
- dependencies:
- kind-of "^3.0.2"
- longest "^1.0.1"
- repeat-string "^1.5.2"
-
ansi-escapes@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b"
@@ -84,24 +66,6 @@ ansi-styles@^3.1.0:
dependencies:
color-convert "^1.9.0"
-anymatch@^1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507"
- dependencies:
- arrify "^1.0.0"
- micromatch "^2.1.5"
-
-aproba@^1.0.3:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1"
-
-are-we-there-yet@~1.1.2:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
- dependencies:
- delegates "^1.0.0"
- readable-stream "^2.0.6"
-
argparse@^1.0.7:
version "1.0.9"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
@@ -136,54 +100,6 @@ arrify@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
-asn1.js@^4.0.0:
- version "4.9.1"
- resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40"
- dependencies:
- bn.js "^4.0.0"
- inherits "^2.0.1"
- minimalistic-assert "^1.0.0"
-
-asn1@~0.2.3:
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
-
-assert-plus@1.0.0, assert-plus@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
-
-assert-plus@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
-
-assert@^1.1.1:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
- dependencies:
- util "0.10.3"
-
-async-each@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
-
-async@^2.1.2:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
- dependencies:
- lodash "^4.14.0"
-
-asynckit@^0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
-
-aws-sign2@~0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
-
-aws4@^1.2.1:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
-
babel-code-frame@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
@@ -330,14 +246,6 @@ babel-helpers@^6.24.1:
babel-runtime "^6.22.0"
babel-template "^6.24.1"
-babel-loader@^7.1.1:
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.1.tgz#b87134c8b12e3e4c2a94e0546085bc680a2b8488"
- dependencies:
- find-cache-dir "^1.0.0"
- loader-utils "^1.0.2"
- mkdirp "^0.5.1"
-
babel-messages@^6.23.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
@@ -350,6 +258,12 @@ babel-plugin-check-es2015-constants@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
+babel-plugin-external-helpers@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-external-helpers/-/babel-plugin-external-helpers-6.22.0.tgz#2285f48b02bd5dede85175caf8c62e86adccefa1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
babel-plugin-syntax-async-functions@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
@@ -654,40 +568,6 @@ balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
-base64-js@^1.0.2:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
-
-bcrypt-pbkdf@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
- dependencies:
- tweetnacl "^0.14.3"
-
-big.js@^3.1.3:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978"
-
-binary-extensions@^1.0.0:
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.9.0.tgz#66506c16ce6f4d6928a5b3cd6a33ca41e941e37b"
-
-block-stream@*:
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
- dependencies:
- inherits "~2.0.0"
-
-bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
- version "4.11.7"
- resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.7.tgz#ddb048e50d9482790094c13eb3fcfc833ce7ab46"
-
-boom@2.x.x:
- version "2.10.1"
- resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
- dependencies:
- hoek "2.x.x"
-
brace-expansion@^1.1.7:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
@@ -703,61 +583,6 @@ braces@^1.8.2:
preserve "^0.2.0"
repeat-element "^1.1.2"
-brorand@^1.0.1:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
-
-browserify-aes@^1.0.0, browserify-aes@^1.0.4:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.6.tgz#5e7725dbdef1fd5930d4ebab48567ce451c48a0a"
- dependencies:
- buffer-xor "^1.0.2"
- cipher-base "^1.0.0"
- create-hash "^1.1.0"
- evp_bytestokey "^1.0.0"
- inherits "^2.0.1"
-
-browserify-cipher@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a"
- dependencies:
- browserify-aes "^1.0.4"
- browserify-des "^1.0.0"
- evp_bytestokey "^1.0.0"
-
-browserify-des@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd"
- dependencies:
- cipher-base "^1.0.1"
- des.js "^1.0.0"
- inherits "^2.0.1"
-
-browserify-rsa@^4.0.0:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
- dependencies:
- bn.js "^4.1.0"
- randombytes "^2.0.1"
-
-browserify-sign@^4.0.0:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
- dependencies:
- bn.js "^4.1.1"
- browserify-rsa "^4.0.0"
- create-hash "^1.1.0"
- create-hmac "^1.1.2"
- elliptic "^6.0.0"
- inherits "^2.0.1"
- parse-asn1 "^5.0.0"
-
-browserify-zlib@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d"
- dependencies:
- pako "~0.2.0"
-
browserslist@^2.1.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.2.2.tgz#e9b4618b8a01c193f9786beea09f6fd10dbe31c3"
@@ -765,25 +590,13 @@ browserslist@^2.1.2:
caniuse-lite "^1.0.30000704"
electron-to-chromium "^1.3.16"
-buffer-xor@^1.0.2:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
-
-buffer@^4.3.0:
- version "4.9.1"
- resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
- dependencies:
- base64-js "^1.0.2"
- ieee754 "^1.1.4"
- isarray "^1.0.0"
-
builtin-modules@^1.0.0, builtin-modules@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
-builtin-status-codes@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+builtin-modules@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-2.0.0.tgz#60b7ef5ae6546bd7deefa74b08b62a43a232648e"
caller-path@^0.1.0:
version "0.1.0"
@@ -795,29 +608,10 @@ callsites@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
-camelcase@^1.0.2:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
-
-camelcase@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
-
caniuse-lite@^1.0.30000704:
version "1.0.30000706"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000706.tgz#bc59abc41ba7d4a3634dda95befded6114e1f24e"
-caseless@~0.12.0:
- version "0.12.0"
- resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
-
-center-align@^0.1.1:
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
- dependencies:
- align-text "^0.1.3"
- lazy-cache "^1.0.3"
-
chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@@ -836,28 +630,6 @@ chalk@^2.0.0:
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
-chokidar@^1.7.0:
- version "1.7.0"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
- dependencies:
- anymatch "^1.3.0"
- async-each "^1.0.0"
- glob-parent "^2.0.0"
- inherits "^2.0.1"
- is-binary-path "^1.0.0"
- is-glob "^2.0.0"
- path-is-absolute "^1.0.0"
- readdirp "^2.0.0"
- optionalDependencies:
- fsevents "^1.0.0"
-
-cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
- dependencies:
- inherits "^2.0.1"
- safe-buffer "^5.0.1"
-
circular-json@^0.3.1:
version "0.3.3"
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
@@ -872,30 +644,10 @@ cli-width@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
-cliui@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
- dependencies:
- center-align "^0.1.1"
- right-align "^0.1.1"
- wordwrap "0.0.2"
-
-cliui@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
- dependencies:
- string-width "^1.0.1"
- strip-ansi "^3.0.1"
- wrap-ansi "^2.0.0"
-
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
-code-point-at@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
-
color-convert@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
@@ -906,15 +658,9 @@ color-name@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
-combined-stream@^1.0.5, combined-stream@~1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
- dependencies:
- delayed-stream "~1.0.0"
-
-commondir@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+commander@~2.13.0:
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
concat-map@0.0.1:
version "0.0.1"
@@ -928,20 +674,6 @@ concat-stream@^1.6.0:
readable-stream "^2.2.2"
typedarray "^0.0.6"
-console-browserify@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
- dependencies:
- date-now "^0.1.4"
-
-console-control-strings@^1.0.0, console-control-strings@~1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
-
-constants-browserify@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
-
contains-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
@@ -958,34 +690,7 @@ core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
-create-ecdh@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
- dependencies:
- bn.js "^4.1.0"
- elliptic "^6.0.0"
-
-create-hash@^1.1.0, create-hash@^1.1.1, create-hash@^1.1.2:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd"
- dependencies:
- cipher-base "^1.0.1"
- inherits "^2.0.1"
- ripemd160 "^2.0.0"
- sha.js "^2.4.0"
-
-create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
- version "1.1.6"
- resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06"
- dependencies:
- cipher-base "^1.0.3"
- create-hash "^1.1.0"
- inherits "^2.0.1"
- ripemd160 "^2.0.0"
- safe-buffer "^5.0.1"
- sha.js "^2.4.8"
-
-cross-spawn@^5.0.1, cross-spawn@^5.1.0:
+cross-spawn@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
dependencies:
@@ -993,57 +698,12 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0:
shebang-command "^1.2.0"
which "^1.2.9"
-cryptiles@2.x.x:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
- dependencies:
- boom "2.x.x"
-
-crypto-browserify@^3.11.0:
- version "3.11.1"
- resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f"
- dependencies:
- browserify-cipher "^1.0.0"
- browserify-sign "^4.0.0"
- create-ecdh "^4.0.0"
- create-hash "^1.1.0"
- create-hmac "^1.1.0"
- diffie-hellman "^5.0.0"
- inherits "^2.0.1"
- pbkdf2 "^3.0.3"
- public-encrypt "^4.0.0"
- randombytes "^2.0.0"
-
-d@1:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
- dependencies:
- es5-ext "^0.10.9"
-
-dashdash@^1.12.0:
- version "1.14.1"
- resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
- dependencies:
- assert-plus "^1.0.0"
-
-date-now@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
-
debug@^2.1.1, debug@^2.2.0, debug@^2.6.8:
version "2.6.8"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
dependencies:
ms "2.0.0"
-decamelize@^1.0.0, decamelize@^1.1.1:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
-
-deep-extend@~0.4.0:
- version "0.4.2"
- resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
-
deep-is@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
@@ -1060,35 +720,12 @@ del@^2.0.2:
pinkie-promise "^2.0.0"
rimraf "^2.2.8"
-delayed-stream@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
-
-delegates@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
-
-des.js@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
- dependencies:
- inherits "^2.0.1"
- minimalistic-assert "^1.0.0"
-
detect-indent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
dependencies:
repeating "^2.0.0"
-diffie-hellman@^5.0.0:
- version "5.0.2"
- resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
- dependencies:
- bn.js "^4.1.0"
- miller-rabin "^4.0.0"
- randombytes "^2.0.0"
-
doctrine@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
@@ -1103,128 +740,20 @@ doctrine@^2.0.0:
esutils "^2.0.2"
isarray "^1.0.0"
-domain-browser@^1.1.1:
- version "1.1.7"
- resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
-
-ecc-jsbn@~0.1.1:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
- dependencies:
- jsbn "~0.1.0"
-
electron-to-chromium@^1.3.16:
version "1.3.16"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.16.tgz#d0e026735754770901ae301a21664cba45d92f7d"
-elliptic@^6.0.0:
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
- dependencies:
- bn.js "^4.4.0"
- brorand "^1.0.1"
- hash.js "^1.0.0"
- hmac-drbg "^1.0.0"
- inherits "^2.0.1"
- minimalistic-assert "^1.0.0"
- minimalistic-crypto-utils "^1.0.0"
-
-emojis-list@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
-
-enhanced-resolve@^3.4.0:
- version "3.4.1"
- resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e"
- dependencies:
- graceful-fs "^4.1.2"
- memory-fs "^0.4.0"
- object-assign "^4.0.1"
- tapable "^0.2.7"
-
-errno@^0.1.3:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
- dependencies:
- prr "~0.0.0"
-
error-ex@^1.2.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
dependencies:
is-arrayish "^0.2.1"
-es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14:
- version "0.10.24"
- resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.24.tgz#a55877c9924bc0c8d9bd3c2cbe17495ac1709b14"
- dependencies:
- es6-iterator "2"
- es6-symbol "~3.1"
-
-es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512"
- dependencies:
- d "1"
- es5-ext "^0.10.14"
- es6-symbol "^3.1"
-
-es6-map@^0.1.3:
- version "0.1.5"
- resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
- dependencies:
- d "1"
- es5-ext "~0.10.14"
- es6-iterator "~2.0.1"
- es6-set "~0.1.5"
- es6-symbol "~3.1.1"
- event-emitter "~0.3.5"
-
-es6-set@~0.1.5:
- version "0.1.5"
- resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
- dependencies:
- d "1"
- es5-ext "~0.10.14"
- es6-iterator "~2.0.1"
- es6-symbol "3.1.1"
- event-emitter "~0.3.5"
-
-es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
- dependencies:
- d "1"
- es5-ext "~0.10.14"
-
-es6-weak-map@^2.0.1:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f"
- dependencies:
- d "1"
- es5-ext "^0.10.14"
- es6-iterator "^2.0.1"
- es6-symbol "^3.1.1"
-
escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
-escope@^3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3"
- dependencies:
- es6-map "^0.1.3"
- es6-weak-map "^2.0.1"
- esrecurse "^4.1.0"
- estraverse "^4.1.1"
-
-eslint-config-airbnb-base@^11.3.1:
- version "11.3.1"
- resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-11.3.1.tgz#c0ab108c9beed503cb999e4c60f4ef98eda0ed30"
- dependencies:
- eslint-restricted-globals "^0.1.1"
-
eslint-import-resolver-node@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz#4422574cde66a9a7b099938ee4d508a199e0e3cc"
@@ -1254,10 +783,6 @@ eslint-plugin-import@^2.7.0:
minimatch "^3.0.3"
read-pkg-up "^2.0.0"
-eslint-restricted-globals@^0.1.1:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7"
-
eslint-scope@^3.7.1:
version "3.7.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
@@ -1334,38 +859,21 @@ estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
-esutils@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
-
-event-emitter@~0.3.5:
- version "0.3.5"
- resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
- dependencies:
- d "1"
- es5-ext "~0.10.14"
+estree-walker@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e"
-events@^1.0.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+estree-walker@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.3.1.tgz#e6b1a51cf7292524e7237c312e5fe6660c1ce1aa"
-evp_bytestokey@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz#497b66ad9fef65cd7c08a6180824ba1476b66e53"
- dependencies:
- create-hash "^1.1.1"
+estree-walker@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.1.tgz#64fc375053abc6f57d73e9bd2f004644ad3c5854"
-execa@^0.7.0:
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
- dependencies:
- cross-spawn "^5.0.1"
- get-stream "^3.0.0"
- is-stream "^1.1.0"
- npm-run-path "^2.0.0"
- p-finally "^1.0.0"
- signal-exit "^3.0.0"
- strip-eof "^1.0.0"
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
expand-brackets@^0.1.4:
version "0.1.5"
@@ -1379,10 +887,6 @@ expand-range@^1.8.1:
dependencies:
fill-range "^2.1.0"
-extend@~3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
-
external-editor@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.4.tgz#1ed9199da9cbfe2ef2f7a31b2fde8b0d12368972"
@@ -1397,10 +901,6 @@ extglob@^0.3.1:
dependencies:
is-extglob "^1.0.0"
-extsprintf@1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550"
-
fast-deep-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
@@ -1436,14 +936,6 @@ fill-range@^2.1.0:
repeat-element "^1.1.2"
repeat-string "^1.5.2"
-find-cache-dir@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f"
- dependencies:
- commondir "^1.0.1"
- make-dir "^1.0.0"
- pkg-dir "^2.0.0"
-
find-up@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
@@ -1451,7 +943,7 @@ find-up@^1.0.0:
path-exists "^2.0.0"
pinkie-promise "^2.0.0"
-find-up@^2.0.0, find-up@^2.1.0:
+find-up@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
dependencies:
@@ -1476,46 +968,10 @@ for-own@^0.1.4:
dependencies:
for-in "^1.0.1"
-forever-agent@~0.6.1:
- version "0.6.1"
- resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
-
-form-data@~2.1.1:
- version "2.1.4"
- resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
- dependencies:
- asynckit "^0.4.0"
- combined-stream "^1.0.5"
- mime-types "^2.1.12"
-
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
-fsevents@^1.0.0:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4"
- dependencies:
- nan "^2.3.0"
- node-pre-gyp "^0.6.36"
-
-fstream-ignore@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
- dependencies:
- fstream "^1.0.0"
- inherits "2"
- minimatch "^3.0.0"
-
-fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
- version "1.0.11"
- resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
- dependencies:
- graceful-fs "^4.1.2"
- inherits "~2.0.0"
- mkdirp ">=0.5 0"
- rimraf "2"
-
function-bind@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
@@ -1524,33 +980,6 @@ functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
-gauge@~2.7.3:
- version "2.7.4"
- resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
- dependencies:
- aproba "^1.0.3"
- console-control-strings "^1.0.0"
- has-unicode "^2.0.0"
- object-assign "^4.1.0"
- signal-exit "^3.0.0"
- string-width "^1.0.1"
- strip-ansi "^3.0.1"
- wide-align "^1.1.0"
-
-get-caller-file@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
-
-get-stream@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
-
-getpass@^0.1.1:
- version "0.1.7"
- resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
- dependencies:
- assert-plus "^1.0.0"
-
glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -1594,17 +1023,6 @@ graceful-fs@^4.1.2:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
-har-schema@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
-
-har-validator@~4.2.1:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
- dependencies:
- ajv "^4.9.1"
- har-schema "^1.0.5"
-
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -1615,50 +1033,12 @@ has-flag@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
-has-unicode@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
-
has@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
dependencies:
function-bind "^1.0.2"
-hash-base@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1"
- dependencies:
- inherits "^2.0.1"
-
-hash.js@^1.0.0, hash.js@^1.0.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
- dependencies:
- inherits "^2.0.3"
- minimalistic-assert "^1.0.0"
-
-hawk@~3.1.3:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
- dependencies:
- boom "2.x.x"
- cryptiles "2.x.x"
- hoek "2.x.x"
- sntp "1.x.x"
-
-hmac-drbg@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
- dependencies:
- hash.js "^1.0.3"
- minimalistic-assert "^1.0.0"
- minimalistic-crypto-utils "^1.0.1"
-
-hoek@2.x.x:
- version "2.16.3"
- resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
-
home-or-tmp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -1670,26 +1050,10 @@ hosted-git-info@^2.1.4:
version "2.5.0"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
-http-signature@~1.1.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
- dependencies:
- assert-plus "^0.2.0"
- jsprim "^1.2.2"
- sshpk "^1.7.0"
-
-https-browserify@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
-
iconv-lite@^0.4.17:
version "0.4.18"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
-ieee754@^1.1.4:
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
-
ignore@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d"
@@ -1698,10 +1062,6 @@ imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
-indexof@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
-
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -1709,18 +1069,10 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@^2.0.3, inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
-inherits@2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
-
-ini@~1.3.0:
- version "1.3.4"
- resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
-
inquirer@^3.0.6:
version "3.2.1"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.1.tgz#06ceb0f540f45ca548c17d6840959878265fa175"
@@ -1740,30 +1092,16 @@ inquirer@^3.0.6:
strip-ansi "^4.0.0"
through "^2.3.6"
-interpret@^1.0.0:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90"
-
invariant@^2.2.0, invariant@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
dependencies:
loose-envify "^1.0.0"
-invert-kv@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
-
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
-is-binary-path@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
- dependencies:
- binary-extensions "^1.0.0"
-
is-buffer@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
@@ -1798,12 +1136,6 @@ is-finite@^1.0.0:
dependencies:
number-is-nan "^1.0.0"
-is-fullwidth-code-point@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
- dependencies:
- number-is-nan "^1.0.0"
-
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
@@ -1814,6 +1146,10 @@ is-glob@^2.0.0, is-glob@^2.0.1:
dependencies:
is-extglob "^1.0.0"
+is-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
+
is-number@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
@@ -1860,14 +1196,6 @@ is-resolvable@^1.0.0:
dependencies:
tryit "^1.0.1"
-is-stream@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
-
-is-typedarray@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
-
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -1882,10 +1210,6 @@ isobject@^2.0.0:
dependencies:
isarray "1.0.0"
-isstream@~0.1.2:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
-
js-tokens@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
@@ -1897,10 +1221,6 @@ js-yaml@^3.8.4:
argparse "^1.0.7"
esprima "^4.0.0"
-jsbn@~0.1.0:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
-
jschardet@^1.4.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.0.tgz#a61f310306a5a71188e1b1acd08add3cfbb08b1e"
@@ -1913,29 +1233,17 @@ jsesc@~0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
-json-loader@^0.5.4:
- version "0.5.7"
- resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
-
json-schema-traverse@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
-json-schema@0.2.3:
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
-
json-stable-stringify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
dependencies:
jsonify "~0.0.0"
-json-stringify-safe@~5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
-
-json5@^0.5.0, json5@^0.5.1:
+json5@^0.5.0:
version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
@@ -1943,15 +1251,6 @@ jsonify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
-jsprim@^1.2.2:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918"
- dependencies:
- assert-plus "1.0.0"
- extsprintf "1.0.2"
- json-schema "0.2.3"
- verror "1.3.6"
-
kind-of@^3.0.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -1964,16 +1263,6 @@ kind-of@^4.0.0:
dependencies:
is-buffer "^1.1.5"
-lazy-cache@^1.0.3:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
-
-lcid@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
- dependencies:
- invert-kv "^1.0.0"
-
levn@^0.3.0, levn@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@@ -1990,18 +1279,6 @@ load-json-file@^2.0.0:
pify "^2.0.0"
strip-bom "^3.0.0"
-loader-runner@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
-
-loader-utils@^1.0.2, loader-utils@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
- dependencies:
- big.js "^3.1.3"
- emojis-list "^2.0.0"
- json5 "^0.5.0"
-
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@@ -2013,14 +1290,10 @@ lodash.cond@^4.3.0:
version "4.5.2"
resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
-lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0:
+lodash@^4.0.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
-longest@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
-
loose-envify@^1.0.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
@@ -2034,26 +1307,13 @@ lru-cache@^4.0.1:
pseudomap "^1.0.2"
yallist "^2.1.2"
-make-dir@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978"
- dependencies:
- pify "^2.3.0"
-
-mem@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
+magic-string@^0.22.4:
+ version "0.22.5"
+ resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
dependencies:
- mimic-fn "^1.0.0"
+ vlq "^0.2.2"
-memory-fs@^0.4.0, memory-fs@~0.4.1:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
- dependencies:
- errno "^0.1.3"
- readable-stream "^2.0.1"
-
-micromatch@^2.1.5:
+micromatch@^2.3.11:
version "2.3.11"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
dependencies:
@@ -2071,36 +1331,11 @@ micromatch@^2.1.5:
parse-glob "^3.0.4"
regex-cache "^0.4.2"
-miller-rabin@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.0.tgz#4a62fb1d42933c05583982f4c716f6fb9e6c6d3d"
- dependencies:
- bn.js "^4.0.0"
- brorand "^1.0.1"
-
-mime-db@~1.29.0:
- version "1.29.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878"
-
-mime-types@^2.1.12, mime-types@~2.1.7:
- version "2.1.16"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23"
- dependencies:
- mime-db "~1.29.0"
-
mimic-fn@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
-minimalistic-assert@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
-
-minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
-
-minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
dependencies:
@@ -2110,11 +1345,7 @@ minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-minimist@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
-
-"mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0:
+mkdirp@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies:
@@ -2128,63 +1359,10 @@ mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
-nan@^2.3.0:
- version "2.6.2"
- resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
-
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
-node-libs-browser@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646"
- dependencies:
- assert "^1.1.1"
- browserify-zlib "^0.1.4"
- buffer "^4.3.0"
- console-browserify "^1.1.0"
- constants-browserify "^1.0.0"
- crypto-browserify "^3.11.0"
- domain-browser "^1.1.1"
- events "^1.0.0"
- https-browserify "0.0.1"
- os-browserify "^0.2.0"
- path-browserify "0.0.0"
- process "^0.11.0"
- punycode "^1.2.4"
- querystring-es3 "^0.2.0"
- readable-stream "^2.0.5"
- stream-browserify "^2.0.1"
- stream-http "^2.3.1"
- string_decoder "^0.10.25"
- timers-browserify "^2.0.2"
- tty-browserify "0.0.0"
- url "^0.11.0"
- util "^0.10.3"
- vm-browserify "0.0.4"
-
-node-pre-gyp@^0.6.36:
- version "0.6.36"
- resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786"
- dependencies:
- mkdirp "^0.5.1"
- nopt "^4.0.1"
- npmlog "^4.0.2"
- rc "^1.1.7"
- request "^2.81.0"
- rimraf "^2.6.1"
- semver "^5.3.0"
- tar "^2.2.1"
- tar-pack "^3.4.0"
-
-nopt@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
- dependencies:
- abbrev "1"
- osenv "^0.1.4"
-
normalize-package-data@^2.3.2:
version "2.4.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
@@ -2200,30 +1378,11 @@ normalize-path@^2.0.1:
dependencies:
remove-trailing-separator "^1.0.1"
-npm-run-path@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
- dependencies:
- path-key "^2.0.0"
-
-npmlog@^4.0.2:
- version "4.1.2"
- resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
- dependencies:
- are-we-there-yet "~1.1.2"
- console-control-strings "~1.1.0"
- gauge "~2.7.3"
- set-blocking "~2.0.0"
-
number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
-oauth-sign@~0.8.1:
- version "0.8.2"
- resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
-
-object-assign@^4.0.1, object-assign@^4.1.0:
+object-assign@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -2234,7 +1393,7 @@ object.omit@^2.0.0:
for-own "^0.1.4"
is-extendable "^0.1.1"
-once@^1.3.0, once@^1.3.3:
+once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies:
@@ -2257,37 +1416,14 @@ optionator@^0.8.2:
type-check "~0.3.2"
wordwrap "~1.0.0"
-os-browserify@^0.2.0:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
-
os-homedir@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
-os-locale@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
- dependencies:
- execa "^0.7.0"
- lcid "^1.0.0"
- mem "^1.1.0"
-
-os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
+os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
-osenv@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
- dependencies:
- os-homedir "^1.0.0"
- os-tmpdir "^1.0.0"
-
-p-finally@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
-
p-limit@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
@@ -2298,20 +1434,6 @@ p-locate@^2.0.0:
dependencies:
p-limit "^1.1.0"
-pako@~0.2.0:
- version "0.2.9"
- resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
-
-parse-asn1@^5.0.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712"
- dependencies:
- asn1.js "^4.0.0"
- browserify-aes "^1.0.0"
- create-hash "^1.1.0"
- evp_bytestokey "^1.0.0"
- pbkdf2 "^3.0.3"
-
parse-glob@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
@@ -2327,10 +1449,6 @@ parse-json@^2.2.0:
dependencies:
error-ex "^1.2.0"
-path-browserify@0.0.0:
- version "0.0.0"
- resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
-
path-exists@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
@@ -2349,10 +1467,6 @@ path-is-inside@^1.0.1, path-is-inside@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
-path-key@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
-
path-parse@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
@@ -2363,21 +1477,7 @@ path-type@^2.0.0:
dependencies:
pify "^2.0.0"
-pbkdf2@^3.0.3:
- version "3.0.12"
- resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.12.tgz#be36785c5067ea48d806ff923288c5f750b6b8a2"
- dependencies:
- create-hash "^1.1.2"
- create-hmac "^1.1.4"
- ripemd160 "^2.0.1"
- safe-buffer "^5.0.1"
- sha.js "^2.4.8"
-
-performance-now@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
-
-pify@^2.0.0, pify@^2.3.0:
+pify@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -2397,12 +1497,6 @@ pkg-dir@^1.0.0:
dependencies:
find-up "^1.0.0"
-pkg-dir@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
- dependencies:
- find-up "^2.1.0"
-
pluralize@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-4.0.0.tgz#59b708c1c0190a2f692f1c7618c446b052fd1762"
@@ -2423,52 +1517,14 @@ process-nextick-args@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
-process@^0.11.0:
- version "0.11.10"
- resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
-
progress@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
-prr@~0.0.0:
- version "0.0.0"
- resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
-
pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
-public-encrypt@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
- dependencies:
- bn.js "^4.1.0"
- browserify-rsa "^4.0.0"
- create-hash "^1.1.0"
- parse-asn1 "^5.0.0"
- randombytes "^2.0.1"
-
-punycode@1.3.2:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
-
-punycode@^1.2.4, punycode@^1.4.1:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
-
-qs@~6.4.0:
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
-
-querystring-es3@^0.2.0:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
-
-querystring@0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
-
randomatic@^1.1.3:
version "1.1.7"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
@@ -2476,21 +1532,6 @@ randomatic@^1.1.3:
is-number "^3.0.0"
kind-of "^4.0.0"
-randombytes@^2.0.0, randombytes@^2.0.1:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79"
- dependencies:
- safe-buffer "^5.1.0"
-
-rc@^1.1.7:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
- dependencies:
- deep-extend "~0.4.0"
- ini "~1.3.0"
- minimist "^1.2.0"
- strip-json-comments "~2.0.1"
-
read-pkg-up@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
@@ -2506,7 +1547,7 @@ read-pkg@^2.0.0:
normalize-package-data "^2.3.2"
path-type "^2.0.0"
-readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6:
+readable-stream@^2.2.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
dependencies:
@@ -2518,15 +1559,6 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable
string_decoder "~1.0.3"
util-deprecate "~1.0.1"
-readdirp@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
- dependencies:
- graceful-fs "^4.1.2"
- minimatch "^3.0.2"
- readable-stream "^2.0.2"
- set-immediate-shim "^1.0.1"
-
regenerate@^1.2.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260"
@@ -2586,41 +1618,6 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
-request@^2.81.0:
- version "2.81.0"
- resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
- dependencies:
- aws-sign2 "~0.6.0"
- aws4 "^1.2.1"
- caseless "~0.12.0"
- combined-stream "~1.0.5"
- extend "~3.0.0"
- forever-agent "~0.6.1"
- form-data "~2.1.1"
- har-validator "~4.2.1"
- hawk "~3.1.3"
- http-signature "~1.1.0"
- is-typedarray "~1.0.0"
- isstream "~0.1.2"
- json-stringify-safe "~5.0.1"
- mime-types "~2.1.7"
- oauth-sign "~0.8.1"
- performance-now "^0.2.0"
- qs "~6.4.0"
- safe-buffer "^5.0.1"
- stringstream "~0.0.4"
- tough-cookie "~2.3.0"
- tunnel-agent "^0.6.0"
- uuid "^3.0.0"
-
-require-directory@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
-
-require-main-filename@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
-
require-uncached@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
@@ -2632,6 +1629,12 @@ resolve-from@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
+resolve@^1.1.6, resolve@^1.5.0:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
+ dependencies:
+ path-parse "^1.0.5"
+
resolve@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86"
@@ -2645,24 +1648,61 @@ restore-cursor@^2.0.0:
onetime "^2.0.0"
signal-exit "^3.0.2"
-right-align@^0.1.1:
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
- dependencies:
- align-text "^0.1.1"
-
-rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1:
+rimraf@^2.2.8:
version "2.6.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
dependencies:
glob "^7.0.5"
-ripemd160@^2.0.0, ripemd160@^2.0.1:
+rollup-plugin-babel@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-3.0.4.tgz#41b3e762fe64450dd61da3105a2cf7ad76be4edc"
+ dependencies:
+ rollup-pluginutils "^1.5.0"
+
+rollup-plugin-commonjs@^9.1.0:
+ version "9.1.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.0.tgz#468341aab32499123ee9a04b22f51d9bf26fdd94"
+ dependencies:
+ estree-walker "^0.5.1"
+ magic-string "^0.22.4"
+ resolve "^1.5.0"
+ rollup-pluginutils "^2.0.1"
+
+rollup-plugin-node-resolve@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.3.0.tgz#c26d110a36812cbefa7ce117cadcd3439aa1c713"
+ dependencies:
+ builtin-modules "^2.0.0"
+ is-module "^1.0.0"
+ resolve "^1.1.6"
+
+rollup-plugin-uglify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-uglify/-/rollup-plugin-uglify-3.0.0.tgz#a34eca24617709c6bf1778e9653baafa06099b86"
+ dependencies:
+ uglify-es "^3.3.7"
+
+rollup-pluginutils@^1.5.0:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz#1e156e778f94b7255bfa1b3d0178be8f5c552408"
+ dependencies:
+ estree-walker "^0.2.1"
+ minimatch "^3.0.2"
+
+rollup-pluginutils@^2.0.1:
version "2.0.1"
- resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
+ resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.0.1.tgz#7ec95b3573f6543a46a6461bd9a7c544525d0fc0"
+ dependencies:
+ estree-walker "^0.3.0"
+ micromatch "^2.3.11"
+
+rollup@^0.58.2:
+ version "0.58.2"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.58.2.tgz#2feddea8c0c022f3e74b35c48e3c21b3433803ce"
dependencies:
- hash-base "^2.0.0"
- inherits "^2.0.1"
+ "@types/estree" "0.0.38"
+ "@types/node" "*"
run-async@^2.2.0:
version "2.3.0"
@@ -2680,7 +1720,7 @@ rx-lite@*, rx-lite@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
-safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
@@ -2688,24 +1728,6 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.4.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
-set-blocking@^2.0.0, set-blocking@~2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
-
-set-immediate-shim@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
-
-setimmediate@^1.0.4:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
-
-sha.js@^2.4.0, sha.js@^2.4.8:
- version "2.4.8"
- resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f"
- dependencies:
- inherits "^2.0.1"
-
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -2716,7 +1738,7 @@ shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
-signal-exit@^3.0.0, signal-exit@^3.0.2:
+signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
@@ -2728,26 +1750,20 @@ slice-ansi@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
-sntp@1.x.x:
- version "1.0.9"
- resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
- dependencies:
- hoek "2.x.x"
-
-source-list-map@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
-
source-map-support@^0.4.2:
version "0.4.15"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1"
dependencies:
source-map "^0.5.6"
-source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3:
+source-map@^0.5.0, source-map@^0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
+source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+
spark-md5@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.0.tgz#3722227c54e2faf24b1dc6d933cc144e6f71bfef"
@@ -2770,45 +1786,6 @@ sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
-sshpk@^1.7.0:
- version "1.13.1"
- resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
- dependencies:
- asn1 "~0.2.3"
- assert-plus "^1.0.0"
- dashdash "^1.12.0"
- getpass "^0.1.1"
- optionalDependencies:
- bcrypt-pbkdf "^1.0.0"
- ecc-jsbn "~0.1.1"
- jsbn "~0.1.0"
- tweetnacl "~0.14.0"
-
-stream-browserify@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
- dependencies:
- inherits "~2.0.1"
- readable-stream "^2.0.2"
-
-stream-http@^2.3.1:
- version "2.7.2"
- resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad"
- dependencies:
- builtin-status-codes "^3.0.0"
- inherits "^2.0.1"
- readable-stream "^2.2.6"
- to-arraybuffer "^1.0.0"
- xtend "^4.0.0"
-
-string-width@^1.0.1, string-width@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
- dependencies:
- code-point-at "^1.0.0"
- is-fullwidth-code-point "^1.0.0"
- strip-ansi "^3.0.0"
-
string-width@^2.0.0, string-width@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
@@ -2816,21 +1793,13 @@ string-width@^2.0.0, string-width@^2.1.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
-string_decoder@^0.10.25:
- version "0.10.31"
- resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
-
string_decoder@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
dependencies:
safe-buffer "~5.1.0"
-stringstream@~0.0.4:
- version "0.0.5"
- resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
-
-strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+strip-ansi@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
dependencies:
@@ -2846,10 +1815,6 @@ strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
-strip-eof@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
-
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -2858,7 +1823,7 @@ supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
-supports-color@^4.0.0, supports-color@^4.2.1:
+supports-color@^4.0.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836"
dependencies:
@@ -2875,31 +1840,6 @@ table@^4.0.1:
slice-ansi "0.0.4"
string-width "^2.0.0"
-tapable@^0.2.7:
- version "0.2.7"
- resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.7.tgz#e46c0daacbb2b8a98b9b0cea0f4052105817ed5c"
-
-tar-pack@^3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
- dependencies:
- debug "^2.2.0"
- fstream "^1.0.10"
- fstream-ignore "^1.0.5"
- once "^1.3.3"
- readable-stream "^2.1.4"
- rimraf "^2.5.1"
- tar "^2.2.1"
- uid-number "^0.0.6"
-
-tar@^2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
- dependencies:
- block-stream "*"
- fstream "^1.0.2"
- inherits "2"
-
text-table@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -2908,32 +1848,16 @@ through@^2.3.6:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
-timers-browserify@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.2.tgz#ab4883cf597dcd50af211349a00fbca56ac86b86"
- dependencies:
- setimmediate "^1.0.4"
-
tmp@^0.0.31:
version "0.0.31"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
dependencies:
os-tmpdir "~1.0.1"
-to-arraybuffer@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
-
to-fast-properties@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
-tough-cookie@~2.3.0:
- version "2.3.2"
- resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a"
- dependencies:
- punycode "^1.4.1"
-
trim-right@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
@@ -2942,20 +1866,6 @@ tryit@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
-tty-browserify@0.0.0:
- version "0.0.0"
- resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
-
-tunnel-agent@^0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
- dependencies:
- safe-buffer "^5.0.1"
-
-tweetnacl@^0.14.3, tweetnacl@~0.14.0:
- version "0.14.5"
- resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
-
type-check@~0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
@@ -2966,52 +1876,17 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-uglify-js@^2.8.29:
- version "2.8.29"
- resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
- dependencies:
- source-map "~0.5.1"
- yargs "~3.10.0"
- optionalDependencies:
- uglify-to-browserify "~1.0.0"
-
-uglify-to-browserify@~1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
-
-uglifyjs-webpack-plugin@^0.4.6:
- version "0.4.6"
- resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309"
+uglify-es@^3.3.7:
+ version "3.3.9"
+ resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
dependencies:
- source-map "^0.5.6"
- uglify-js "^2.8.29"
- webpack-sources "^1.0.1"
-
-uid-number@^0.0.6:
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
-
-url@^0.11.0:
- version "0.11.0"
- resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
- dependencies:
- punycode "1.3.2"
- querystring "0.2.0"
+ commander "~2.13.0"
+ source-map "~0.6.1"
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
-util@0.10.3, util@^0.10.3:
- version "0.10.3"
- resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
- dependencies:
- inherits "2.0.1"
-
-uuid@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
-
validate-npm-package-license@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
@@ -3019,63 +1894,9 @@ validate-npm-package-license@^3.0.1:
spdx-correct "~1.0.0"
spdx-expression-parse "~1.0.0"
-verror@1.3.6:
- version "1.3.6"
- resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c"
- dependencies:
- extsprintf "1.0.2"
-
-vm-browserify@0.0.4:
- version "0.0.4"
- resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
- dependencies:
- indexof "0.0.1"
-
-watchpack@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac"
- dependencies:
- async "^2.1.2"
- chokidar "^1.7.0"
- graceful-fs "^4.1.2"
-
-webpack-sources@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf"
- dependencies:
- source-list-map "^2.0.0"
- source-map "~0.5.3"
-
-webpack@^3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.4.0.tgz#e9465b660ad79dd2d33874d968b31746ea9a8e63"
- dependencies:
- acorn "^5.0.0"
- acorn-dynamic-import "^2.0.0"
- ajv "^5.1.5"
- ajv-keywords "^2.0.0"
- async "^2.1.2"
- enhanced-resolve "^3.4.0"
- escope "^3.6.0"
- interpret "^1.0.0"
- json-loader "^0.5.4"
- json5 "^0.5.1"
- loader-runner "^2.3.0"
- loader-utils "^1.1.0"
- memory-fs "~0.4.1"
- mkdirp "~0.5.0"
- node-libs-browser "^2.0.0"
- source-map "^0.5.3"
- supports-color "^4.2.1"
- tapable "^0.2.7"
- uglifyjs-webpack-plugin "^0.4.6"
- watchpack "^1.4.0"
- webpack-sources "^1.0.1"
- yargs "^8.0.2"
-
-which-module@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+vlq@^0.2.2:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
which@^1.2.9:
version "1.2.14"
@@ -3083,31 +1904,10 @@ which@^1.2.9:
dependencies:
isexe "^2.0.0"
-wide-align@^1.1.0:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
- dependencies:
- string-width "^1.0.2"
-
-window-size@0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
-
-wordwrap@0.0.2:
- version "0.0.2"
- resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
-
wordwrap@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
-wrap-ansi@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
- dependencies:
- string-width "^1.0.1"
- strip-ansi "^3.0.1"
-
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@@ -3118,47 +1918,6 @@ write@^0.2.1:
dependencies:
mkdirp "^0.5.1"
-xtend@^4.0.0:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
-
-y18n@^3.2.1:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
-
yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
-
-yargs-parser@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
- dependencies:
- camelcase "^4.1.0"
-
-yargs@^8.0.2:
- version "8.0.2"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
- dependencies:
- camelcase "^4.1.0"
- cliui "^3.2.0"
- decamelize "^1.1.1"
- get-caller-file "^1.0.1"
- os-locale "^2.0.0"
- read-pkg-up "^2.0.0"
- require-directory "^2.1.1"
- require-main-filename "^1.0.1"
- set-blocking "^2.0.0"
- string-width "^2.0.0"
- which-module "^2.0.0"
- y18n "^3.2.1"
- yargs-parser "^7.0.0"
-
-yargs@~3.10.0:
- version "3.10.0"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
- dependencies:
- camelcase "^1.0.2"
- cliui "^2.1.0"
- decamelize "^1.0.0"
- window-size "0.1.0"
diff --git a/activesupport/.gitignore b/activesupport/.gitignore
new file mode 100644
index 0000000000..8cde8514fd
--- /dev/null
+++ b/activesupport/.gitignore
@@ -0,0 +1 @@
+/test/fixtures/isolation_test/
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index f158d5357d..62c0f612a8 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,209 +1,109 @@
-* Add `config/credentials.yml.enc` to store production app secrets.
+* Fix bug where `ActiveSupport::Timezone.all` would fail when tzinfo data for
+ any timezone defined in `ActiveSupport::TimeZone::MAPPING` is missing.
- Allows saving any authentication credentials for third party services
- directly in repo encrypted with `config/master.key` or `ENV["RAILS_MASTER_KEY"]`.
+ *Dominik Sander*
- This will eventually replace `Rails.application.secrets` and the encrypted
- secrets introduced in Rails 5.1.
+* Redis cache store: `delete_matched` no longer blocks the Redis server.
+ (Switches from evaled Lua to a batched SCAN + DEL loop.)
- *DHH*, *Kasper Timm Hansen*
+ *Gleb Mazovetskiy*
-* Add `ActiveSupport::EncryptedFile` and `ActiveSupport::EncryptedConfiguration`.
+* Fix bug where `ActiveSupport::Cache` will massively inflate the storage
+ size when compression is enabled (which is true by default). This patch
+ does not attempt to repair existing data: please manually flush the cache
+ to clear out the problematic entries.
- Allows for stashing encrypted files or configuration directly in repo by
- encrypting it with a key.
+ *Godfrey Chan*
- Backs the new credentials setup above, but can also be used independently.
+* Fix bug where `URI.unscape` would fail with mixed Unicode/escaped character input:
- *DHH*, *Kasper Timm Hansen*
+ URI.unescape("\xe3\x83\x90") # => "バ"
+ URI.unescape("%E3%83%90") # => "バ"
+ URI.unescape("\xe3\x83\x90%E3%83%90") # => Encoding::CompatibilityError
-* `Module#delegate_missing_to` now raises `DelegationError` if target is nil,
- similar to `Module#delegate`.
+ *Ashe Connor*, *Aaron Patterson*
- *Anton Khamets*
+* Add `before?` and `after?` methods to `Date`, `DateTime`,
+ `Time`, and `TimeWithZone`.
-* Update `String#camelize` to provide feedback when wrong option is passed
+ *Nick Holden*
- `String#camelize` was returning nil without any feedback when an
- invalid option was passed as a parameter.
+* `ActiveSupport::Inflector#ordinal` and `ActiveSupport::Inflector#ordinalize` now support
+ translations through I18n.
- Previously:
+ # locale/fr.rb
- 'one_two'.camelize(true)
- => nil
+ {
+ fr: {
+ number: {
+ nth: {
+ ordinals: lambda do |_key, number:, **_options|
+ if number.to_i.abs == 1
+ 'er'
+ else
+ 'e'
+ end
+ end,
- Now:
+ ordinalized: lambda do |_key, number:, **_options|
+ "#{number}#{ActiveSupport::Inflector.ordinal(number)}"
+ end
+ }
+ }
+ }
+ }
- 'one_two'.camelize(true)
- => ArgumentError: Invalid option, use either :upper or :lower.
- *Ricardo Díaz*
+ *Christian Blais*
-* Fix modulo operations involving durations
+* Add `:private` option to ActiveSupport's `Module#delegate`
+ in order to delegate methods as private:
- Rails 5.1 introduced `ActiveSupport::Duration::Scalar` as a wrapper
- around numeric values as a way of ensuring a duration was the outcome of
- an expression. However, the implementation was missing support for modulo
- operations. This support has now been added and should result in a duration
- being returned from expressions involving modulo operations.
+ class User < ActiveRecord::Base
+ has_one :profile
+ delegate :date_of_birth, to: :profile, private: true
- Prior to Rails 5.1:
+ def age
+ Date.today.year - date_of_birth.year
+ end
+ end
- 5.minutes % 2.minutes
- => 60
+ # User.new.age # => 29
+ # User.new.date_of_birth
+ # => NoMethodError: private method `date_of_birth' called for #<User:0x00000008221340>
- Now:
+ *Tomas Valent*
- 5.minutes % 2.minutes
- => 1 minute
-
- Fixes #29603 and #29743.
-
- *Sayan Chakraborty*, *Andrew White*
-
-* Fix division where a duration is the denominator
-
- PR #29163 introduced a change in behavior when a duration was the denominator
- in a calculation - this was incorrect as dividing by a duration should always
- return a `Numeric`. The behavior of previous versions of Rails has been restored.
-
- Fixes #29592.
-
- *Andrew White*
-
-* Add purpose and expiry support to `ActiveSupport::MessageVerifier` &
- `ActiveSupport::MessageEncryptor`.
-
- For instance, to ensure a message is only usable for one intended purpose:
-
- token = @verifier.generate("x", purpose: :shipping)
-
- @verifier.verified(token, purpose: :shipping) # => "x"
- @verifier.verified(token) # => nil
-
- Or make it expire after a set time:
-
- @verifier.generate("x", expires_in: 1.month)
- @verifier.generate("y", expires_at: Time.now.end_of_year)
-
- Showcased with `ActiveSupport::MessageVerifier`, but works the same for
- `ActiveSupport::MessageEncryptor`'s `encrypt_and_sign` and `decrypt_and_verify`.
-
- Pull requests: #29599, #29854
-
- *Assain Jaleel*
-
-* Make the order of `Hash#reverse_merge!` consistent with `HashWithIndifferentAccess`.
-
- *Erol Fornoles*
-
-* Add `freeze_time` helper which freezes time to `Time.now` in tests.
-
- *Prathamesh Sonpatki*
-
-* Default `ActiveSupport::MessageEncryptor` to use AES 256 GCM encryption.
-
- On for new Rails 5.2 apps. Upgrading apps can find the config as a new
- framework default.
-
- *Assain Jaleel*
-
-* Cache: `write_multi`
-
- Rails.cache.write_multi foo: 'bar', baz: 'qux'
-
- Plus faster fetch_multi with stores that implement `write_multi_entries`.
- Keys that aren't found may be written to the cache store in one shot
- instead of separate writes.
-
- The default implementation simply calls `write_entry` for each entry.
- Stores may override if they're capable of one-shot bulk writes, like
- Redis `MSET`.
+* `String#truncate_bytes` to truncate a string to a maximum bytesize without
+ breaking multibyte characters or grapheme clusters like 👩‍👩‍👦‍👦.
*Jeremy Daer*
-* Add default option to module and class attribute accessors.
+* `String#strip_heredoc` preserves frozenness.
- mattr_accessor :settings, default: {}
+ "foo".freeze.strip_heredoc.frozen? # => true
- Works for `mattr_reader`, `mattr_writer`, `cattr_accessor`, `cattr_reader`,
- and `cattr_writer` as well.
+ Fixes that frozen string literals would inadvertently become unfrozen:
- *Genadi Samokovarov*
+ # frozen_string_literal: true
-* Add `Date#prev_occurring` and `Date#next_occurring` to return specified next/previous occurring day of week.
+ foo = <<-MSG.strip_heredoc
+ la la la
+ MSG
- *Shota Iguchi*
+ foo.frozen? # => false !??
-* Add default option to `class_attribute`.
-
- Before:
-
- class_attribute :settings
- self.settings = {}
-
- Now:
-
- class_attribute :settings, default: {}
-
- *DHH*
-
-* `#singularize` and `#pluralize` now respect uncountables for the specified locale.
-
- *Eilis Hamilton*
-
-* Add `ActiveSupport::CurrentAttributes` to provide a thread-isolated attributes singleton.
- Primary use case is keeping all the per-request attributes easily available to the whole system.
-
- *DHH*
-
-* Fix implicit coercion calculations with scalars and durations
-
- Previously, calculations where the scalar is first would be converted to a duration
- of seconds, but this causes issues with dates being converted to times, e.g:
-
- Time.zone = "Beijing" # => Asia/Shanghai
- date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017
- 2 * 1.day # => 172800 seconds
- date + 2 * 1.day # => Mon, 22 May 2017 00:00:00 CST +08:00
-
- Now, the `ActiveSupport::Duration::Scalar` calculation methods will try to maintain
- the part structure of the duration where possible, e.g:
-
- Time.zone = "Beijing" # => Asia/Shanghai
- date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017
- 2 * 1.day # => 2 days
- date + 2 * 1.day # => Mon, 22 May 2017
-
- Fixes #29160, #28970.
-
- *Andrew White*
-
-* Add support for versioned cache entries. This enables the cache stores to recycle cache keys, greatly saving
- on storage in cases with frequent churn. Works together with the separation of `#cache_key` and `#cache_version`
- in Active Record and its use in Action Pack's fragment caching.
-
- *DHH*
-
-* Pass gem name and deprecation horizon to deprecation notifications.
-
- *Willem van Bergen*
-
-* Add support for `:offset` and `:zone` to `ActiveSupport::TimeWithZone#change`
-
- *Andrew White*
-
-* Add support for `:offset` to `Time#change`
+ *Jeremy Daer*
- Fixes #28723.
+* Rails 6 requires Ruby 2.4.1 or newer.
- *Andrew White*
+ *Jeremy Daer*
-* Add `fetch_values` for `HashWithIndifferentAccess`
+* Adds parallel testing to Rails.
- The method was originally added to `Hash` in Ruby 2.3.0.
+ Parallelize your test suite with forked processes or threads.
- *Josh Pencheon*
+ *Eileen M. Uchitelle*, *Aaron Patterson*
-Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activesupport/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activesupport/CHANGELOG.md) for previous changes.
diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE
index 6b3cead1a7..8f769c0767 100644
--- a/activesupport/MIT-LICENSE
+++ b/activesupport/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2005-2017 David Heinemeier Hansson
+Copyright (c) 2005-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc
index 8b47933bd2..c770324be8 100644
--- a/activesupport/README.rdoc
+++ b/activesupport/README.rdoc
@@ -30,7 +30,7 @@ API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/activesupport/Rakefile b/activesupport/Rakefile
index 8672ab1542..f10f19be0a 100644
--- a/activesupport/Rakefile
+++ b/activesupport/Rakefile
@@ -14,10 +14,30 @@ Rake::TestTask.new do |t|
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
+Rake::Task[:test].enhance do
+ Rake::Task["test:cache_stores:redis:ruby"].invoke
+end
+
namespace :test do
task :isolated do
Dir.glob("test/**/*_test.rb").all? do |file|
sh(Gem.ruby, "-w", "-Ilib:test", file)
end || raise("Failures")
end
+
+ namespace :cache_stores do
+ namespace :redis do
+ %w[ ruby hiredis ].each do |driver|
+ task("env:#{driver}") { ENV["REDIS_DRIVER"] = driver }
+
+ Rake::TestTask.new(driver => "env:#{driver}") do |t|
+ t.libs << "test"
+ t.test_files = ["test/cache/stores/redis_cache_store_test.rb"]
+ t.warning = true
+ t.verbose = true
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
+ end
+ end
+ end
+ end
end
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index db801d2da2..aa695c98b2 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "A toolkit of support libraries and Ruby core extensions extracted from the Rails framework."
s.description = "A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. Rich support for multibyte strings, internationalization, time zones, and testing."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
@@ -27,7 +27,7 @@ Gem::Specification.new do |s|
"changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activesupport/CHANGELOG.md"
}
- s.add_dependency "i18n", "~> 0.7"
+ s.add_dependency "i18n", ">= 0.7", "< 2"
s.add_dependency "tzinfo", "~> 1.1"
s.add_dependency "minitest", "~> 5.1"
s.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2"
diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables
deleted file mode 100755
index 18199b2171..0000000000
--- a/activesupport/bin/generate_tables
+++ /dev/null
@@ -1,141 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-begin
- $:.unshift(File.expand_path("../lib", __dir__))
- require "active_support"
-rescue IOError
-end
-
-require "open-uri"
-require "tmpdir"
-require "fileutils"
-
-module ActiveSupport
- module Multibyte
- module Unicode
- class UnicodeDatabase
- def load; end
- end
-
- class DatabaseGenerator
- BASE_URI = "http://www.unicode.org/Public/#{UNICODE_VERSION}/ucd/"
- SOURCES = {
- codepoints: BASE_URI + "UnicodeData.txt",
- composition_exclusion: BASE_URI + "CompositionExclusions.txt",
- grapheme_break_property: BASE_URI + "auxiliary/GraphemeBreakProperty.txt",
- cp1252: "http://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT"
- }
-
- def initialize
- @ucd = Unicode::UnicodeDatabase.new
- end
-
- def parse_codepoints(line)
- codepoint = Codepoint.new
- raise "Could not parse input." unless line =~ /^
- ([0-9A-F]+); # code
- ([^;]+); # name
- ([A-Z]+); # general category
- ([0-9]+); # canonical combining class
- ([A-Z]+); # bidi class
- (<([A-Z]*)>)? # decomposition type
- ((\ ?[0-9A-F]+)*); # decomposition mapping
- ([0-9]*); # decimal digit
- ([0-9]*); # digit
- ([^;]*); # numeric
- ([YN]*); # bidi mirrored
- ([^;]*); # unicode 1.0 name
- ([^;]*); # iso comment
- ([0-9A-F]*); # simple uppercase mapping
- ([0-9A-F]*); # simple lowercase mapping
- ([0-9A-F]*)$/ix # simple titlecase mapping
- codepoint.code = $1.hex
- codepoint.combining_class = Integer($4)
- codepoint.decomp_type = $7
- codepoint.decomp_mapping = ($8 == "") ? nil : $8.split.collect(&:hex)
- codepoint.uppercase_mapping = ($16 == "") ? 0 : $16.hex
- codepoint.lowercase_mapping = ($17 == "") ? 0 : $17.hex
- @ucd.codepoints[codepoint.code] = codepoint
- end
-
- def parse_grapheme_break_property(line)
- if line =~ /^([0-9A-F.]+)\s*;\s*([\w]+)\s*#/
- type = $2.downcase.intern
- @ucd.boundary[type] ||= []
- if $1.include? ".."
- parts = $1.split ".."
- @ucd.boundary[type] << (parts[0].hex..parts[1].hex)
- else
- @ucd.boundary[type] << $1.hex
- end
- end
- end
-
- def parse_composition_exclusion(line)
- if line =~ /^([0-9A-F]+)/i
- @ucd.composition_exclusion << $1.hex
- end
- end
-
- def parse_cp1252(line)
- if line =~ /^([0-9A-Fx]+)\s([0-9A-Fx]+)/i
- @ucd.cp1252[$1.hex] = $2.hex
- end
- end
-
- def create_composition_map
- @ucd.codepoints.each do |_, cp|
- if !cp.nil? && cp.combining_class == 0 && cp.decomp_type.nil? && !cp.decomp_mapping.nil? && cp.decomp_mapping.length == 2 && @ucd.codepoints[cp.decomp_mapping[0]].combining_class == 0 && !@ucd.composition_exclusion.include?(cp.code)
- @ucd.composition_map[cp.decomp_mapping[0]] ||= {}
- @ucd.composition_map[cp.decomp_mapping[0]][cp.decomp_mapping[1]] = cp.code
- end
- end
- end
-
- def normalize_boundary_map
- @ucd.boundary.each do |k, v|
- if [:lf, :cr].include? k
- @ucd.boundary[k] = v[0]
- end
- end
- end
-
- def parse
- SOURCES.each do |type, url|
- filename = File.join(Dir.tmpdir, UNICODE_VERSION, "#{url.split('/').last}")
- unless File.exist?(filename)
- $stderr.puts "Downloading #{url.split('/').last}"
- FileUtils.mkdir_p(File.dirname(filename))
- File.open(filename, "wb") do |target|
- open(url) do |source|
- source.each_line { |line| target.write line }
- end
- end
- end
- File.open(filename) do |file|
- file.each_line { |line| send "parse_#{type}".intern, line }
- end
- end
- create_composition_map
- normalize_boundary_map
- end
-
- def dump_to(filename)
- File.open(filename, "wb") do |f|
- f.write Marshal.dump([@ucd.codepoints, @ucd.composition_exclusion, @ucd.composition_map, @ucd.boundary, @ucd.cp1252])
- end
- end
- end
- end
- end
-end
-
-if __FILE__ == $0
- filename = ActiveSupport::Multibyte::Unicode::UnicodeDatabase.filename
- generator = ActiveSupport::Multibyte::Unicode::DatabaseGenerator.new
- generator.parse
- print "Writing to: #{filename}"
- generator.dump_to filename
- puts " (#{File.size(filename)} bytes)"
-end
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index 79b12f36b9..a4fb697669 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2005-2017 David Heinemeier Hansson
+# Copyright (c) 2005-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -24,11 +24,11 @@
#++
require "securerandom"
-require_relative "active_support/dependencies/autoload"
-require_relative "active_support/version"
-require_relative "active_support/logger"
-require_relative "active_support/lazy_load_hooks"
-require_relative "active_support/core_ext/date_and_time/compatibility"
+require "active_support/dependencies/autoload"
+require "active_support/version"
+require "active_support/logger"
+require "active_support/lazy_load_hooks"
+require "active_support/core_ext/date_and_time/compatibility"
module ActiveSupport
extend ActiveSupport::Autoload
@@ -53,6 +53,7 @@ module ActiveSupport
autoload :Callbacks
autoload :Configurable
autoload :Deprecation
+ autoload :Digest
autoload :Gzip
autoload :Inflector
autoload :JSON
@@ -82,18 +83,6 @@ module ActiveSupport
cattr_accessor :test_order # :nodoc:
- def self.halt_callback_chains_on_return_false
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- ActiveSupport.halt_callback_chains_on_return_false is deprecated and will be removed in Rails 5.2.
- MSG
- end
-
- def self.halt_callback_chains_on_return_false=(value)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- ActiveSupport.halt_callback_chains_on_return_false= is deprecated and will be removed in Rails 5.2.
- MSG
- end
-
def self.to_time_preserves_timezone
DateAndTime::Compatibility.preserve_timezone
end
diff --git a/activesupport/lib/active_support/all.rb b/activesupport/lib/active_support/all.rb
index 6638b25419..4adf446af8 100644
--- a/activesupport/lib/active_support/all.rb
+++ b/activesupport/lib/active_support/all.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
require "active_support"
-require_relative "time"
-require_relative "core_ext"
+require "active_support/time"
+require "active_support/core_ext"
diff --git a/activesupport/lib/active_support/benchmarkable.rb b/activesupport/lib/active_support/benchmarkable.rb
index 5573f6750e..f481d68198 100644
--- a/activesupport/lib/active_support/benchmarkable.rb
+++ b/activesupport/lib/active_support/benchmarkable.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "core_ext/benchmark"
-require_relative "core_ext/hash/keys"
+require "active_support/core_ext/benchmark"
+require "active_support/core_ext/hash/keys"
module ActiveSupport
module Benchmarkable
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index 49d8965cb1..d769e2c8ea 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -1,21 +1,22 @@
# frozen_string_literal: true
require "zlib"
-require_relative "core_ext/array/extract_options"
-require_relative "core_ext/array/wrap"
-require_relative "core_ext/module/attribute_accessors"
-require_relative "core_ext/numeric/bytes"
-require_relative "core_ext/numeric/time"
-require_relative "core_ext/object/to_param"
-require_relative "core_ext/string/inflections"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/array/wrap"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/numeric/bytes"
+require "active_support/core_ext/numeric/time"
+require "active_support/core_ext/object/to_param"
+require "active_support/core_ext/string/inflections"
module ActiveSupport
# See ActiveSupport::Cache::Store for documentation.
module Cache
- autoload :FileStore, "active_support/cache/file_store"
- autoload :MemoryStore, "active_support/cache/memory_store"
- autoload :MemCacheStore, "active_support/cache/mem_cache_store"
- autoload :NullStore, "active_support/cache/null_store"
+ autoload :FileStore, "active_support/cache/file_store"
+ autoload :MemoryStore, "active_support/cache/memory_store"
+ autoload :MemCacheStore, "active_support/cache/mem_cache_store"
+ autoload :NullStore, "active_support/cache/null_store"
+ autoload :RedisCacheStore, "active_support/cache/redis_cache_store"
# These options mean something to all cache implementations. Individual cache
# implementations may support additional options.
@@ -148,18 +149,34 @@ module ActiveSupport
# cache.namespace = -> { @last_mod_time } # Set the namespace to a variable
# @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
#
- # Caches can also store values in a compressed format to save space and
- # reduce time spent sending data. Since there is overhead, values must be
- # large enough to warrant compression. To turn on compression either pass
- # <tt>compress: true</tt> in the initializer or as an option to +fetch+
- # or +write+. To specify the threshold at which to compress values, set the
- # <tt>:compress_threshold</tt> option. The default threshold is 16K.
+ # Cached data larger than 1kB are compressed by default. To turn off
+ # compression, pass <tt>compress: false</tt> to the initializer or to
+ # individual +fetch+ or +write+ method calls. The 1kB compression
+ # threshold is configurable with the <tt>:compress_threshold</tt> option,
+ # specified in bytes.
class Store
cattr_accessor :logger, instance_writer: true
attr_reader :silence, :options
alias :silence? :silence
+ class << self
+ private
+ def retrieve_pool_options(options)
+ {}.tap do |pool_options|
+ pool_options[:size] = options.delete(:pool_size) if options[:pool_size]
+ pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout]
+ end
+ end
+
+ def ensure_connection_pool_added!
+ require "connection_pool"
+ rescue LoadError => e
+ $stderr.puts "You don't have connection_pool installed in your application. Please add it to your Gemfile and run bundle install"
+ raise e
+ end
+ end
+
# Creates a new cache. The options will be passed to any write method calls
# except for <tt>:namespace</tt> which can be used to set the global
# namespace for the cache.
@@ -212,8 +229,7 @@ module ActiveSupport
# ask whether you should force a cache write. Otherwise, it's clearer to
# just call <tt>Cache#write</tt>.
#
- # Setting <tt>:compress</tt> will store a large cache entry set by the call
- # in a compressed format.
+ # Setting <tt>compress: false</tt> disables compression of the cache entry.
#
# Setting <tt>:expires_in</tt> will set an expiration time on the cache.
# All caches support auto-expiring content after a specified number of
@@ -317,8 +333,9 @@ module ActiveSupport
# the cache with the given key, then that data is returned. Otherwise,
# +nil+ is returned.
#
- # Note, if data was written with the <tt>:expires_in<tt> or <tt>:version</tt> options,
- # both of these conditions are applied before the data is returned.
+ # Note, if data was written with the <tt>:expires_in</tt> or
+ # <tt>:version</tt> options, both of these conditions are applied before
+ # the data is returned.
#
# Options are passed to the underlying cache implementation.
def read(name, options = nil)
@@ -358,23 +375,11 @@ module ActiveSupport
options = names.extract_options!
options = merged_options(options)
- results = {}
- names.each do |name|
- key = normalize_key(name, options)
- version = normalize_version(name, options)
- entry = read_entry(key, options)
-
- if entry
- if entry.expired?
- delete_entry(key, options)
- elsif entry.mismatched?(version)
- # Skip mismatched versions
- else
- results[name] = entry.value
- end
+ instrument :read_multi, names, options do |payload|
+ read_multi_entries(names, options).tap do |results|
+ payload[:hits] = results.keys
end
end
- results
end
# Cache Storage API to write multiple values at once.
@@ -415,14 +420,19 @@ module ActiveSupport
options = names.extract_options!
options = merged_options(options)
- read_multi(*names, options).tap do |results|
- writes = {}
+ instrument :read_multi, names, options do |payload|
+ read_multi_entries(names, options).tap do |results|
+ payload[:hits] = results.keys
+ payload[:super_operation] = :fetch_multi
- (names - results.keys).each do |name|
- results[name] = writes[name] = yield(name)
- end
+ writes = {}
+
+ (names - results.keys).each do |name|
+ results[name] = writes[name] = yield(name)
+ end
- write_multi writes, options
+ write_multi writes, options
+ end
end
end
@@ -539,6 +549,28 @@ module ActiveSupport
raise NotImplementedError.new
end
+ # Reads multiple entries from the cache implementation. Subclasses MAY
+ # implement this method.
+ def read_multi_entries(names, options)
+ results = {}
+ names.each do |name|
+ key = normalize_key(name, options)
+ version = normalize_version(name, options)
+ entry = read_entry(key, options)
+
+ if entry
+ if entry.expired?
+ delete_entry(key, options)
+ elsif entry.mismatched?(version)
+ # Skip mismatched versions
+ else
+ results[name] = entry.value
+ end
+ end
+ end
+ results
+ end
+
# Writes multiple entries to the cache implementation. Subclasses MAY
# implement this method.
def write_multi_entries(hash, options)
@@ -562,14 +594,34 @@ module ActiveSupport
end
end
- # Prefixes a key with the namespace. Namespace and key will be delimited
- # with a colon.
- def normalize_key(key, options)
- key = expanded_key(key)
- namespace = options[:namespace] if options
- prefix = namespace.is_a?(Proc) ? namespace.call : namespace
- key = "#{prefix}:#{key}" if prefix
- key
+ # Expands and namespaces the cache key. May be overridden by
+ # cache stores to do additional normalization.
+ def normalize_key(key, options = nil)
+ namespace_key expanded_key(key), options
+ end
+
+ # Prefix the key with a namespace string:
+ #
+ # namespace_key 'foo', namespace: 'cache'
+ # # => 'cache:foo'
+ #
+ # With a namespace block:
+ #
+ # namespace_key 'foo', namespace: -> { 'cache' }
+ # # => 'cache:foo'
+ def namespace_key(key, options = nil)
+ options = merged_options(options)
+ namespace = options[:namespace]
+
+ if namespace.respond_to?(:call)
+ namespace = namespace.call
+ end
+
+ if namespace
+ "#{namespace}:#{key}"
+ else
+ key
+ end
end
# Expands key to be a consistent string value. Invokes +cache_key+ if
@@ -658,22 +710,17 @@ module ActiveSupport
class Entry # :nodoc:
attr_reader :version
- DEFAULT_COMPRESS_LIMIT = 16.kilobytes
+ DEFAULT_COMPRESS_LIMIT = 1.kilobyte
# 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)
- @value = compress(value)
- @compressed = true
- else
- @value = value
- end
-
- @version = options[:version]
+ # +:compress+, +:compress_threshold+, +:version+ and +:expires_in+.
+ def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, **)
+ @value = value
+ @version = version
@created_at = Time.now.to_f
- @expires_in = options[:expires_in]
- @expires_in = @expires_in.to_f if @expires_in
+ @expires_in = expires_in && expires_in.to_f
+
+ compress!(compress_threshold) if compress
end
def value
@@ -705,17 +752,13 @@ module ActiveSupport
# Returns the size of the cached value. This could be less than
# <tt>value.size</tt> if the data is compressed.
def size
- if defined?(@s)
- @s
+ case value
+ when NilClass
+ 0
+ when String
+ @value.bytesize
else
- case value
- when NilClass
- 0
- when String
- @value.bytesize
- else
- @s = Marshal.dump(@value).bytesize
- end
+ @s ||= Marshal.dump(@value).bytesize
end
end
@@ -732,23 +775,30 @@ module ActiveSupport
end
private
- def should_compress?(value, options)
- if value && options[:compress]
- compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT
- serialized_value_size = (value.is_a?(String) ? value : Marshal.dump(value)).bytesize
-
- return true if serialized_value_size >= compress_threshold
+ def compress!(compress_threshold)
+ case @value
+ when nil, true, false, Numeric
+ uncompressed_size = 0
+ when String
+ uncompressed_size = @value.bytesize
+ else
+ serialized = Marshal.dump(@value)
+ uncompressed_size = serialized.bytesize
end
- false
- end
+ if uncompressed_size >= compress_threshold
+ serialized ||= Marshal.dump(@value)
+ compressed = Zlib::Deflate.deflate(serialized)
- def compressed?
- defined?(@compressed) ? @compressed : false
+ if compressed.bytesize < uncompressed_size
+ @value = compressed
+ @compressed = true
+ end
+ end
end
- def compress(value)
- Zlib::Deflate.deflate(Marshal.dump(value))
+ def compressed?
+ defined?(@compressed)
end
def uncompress(value)
diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb
index 28df2c066e..a0f44aac0f 100644
--- a/activesupport/lib/active_support/cache/file_store.rb
+++ b/activesupport/lib/active_support/cache/file_store.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "../core_ext/marshal"
-require_relative "../core_ext/file/atomic"
-require_relative "../core_ext/string/conversions"
+require "active_support/core_ext/marshal"
+require "active_support/core_ext/file/atomic"
+require "active_support/core_ext/string/conversions"
require "uri/common"
module ActiveSupport
@@ -39,9 +39,8 @@ module ActiveSupport
def cleanup(options = nil)
options = merged_options(options)
search_dir(cache_path) do |fname|
- key = file_path_key(fname)
- entry = read_entry(key, options)
- delete_entry(key, options) if entry && entry.expired?
+ entry = read_entry(fname, options)
+ delete_entry(fname, options) if entry && entry.expired?
end
end
@@ -122,7 +121,7 @@ module ActiveSupport
fname = URI.encode_www_form_component(key)
if fname.size > FILEPATH_MAX_SIZE
- fname = Digest::MD5.hexdigest(key)
+ fname = ActiveSupport::Digest.hexdigest(key)
end
hash = Zlib.adler32(fname)
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index d7bd914722..2840781dde 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -7,9 +7,8 @@ rescue LoadError => e
raise e
end
-require "digest/md5"
-require_relative "../core_ext/marshal"
-require_relative "../core_ext/array/extract_options"
+require "active_support/core_ext/marshal"
+require "active_support/core_ext/array/extract_options"
module ActiveSupport
module Cache
@@ -64,7 +63,14 @@ module ActiveSupport
addresses = addresses.flatten
options = addresses.extract_options!
addresses = ["localhost:11211"] if addresses.empty?
- Dalli::Client.new(addresses, options)
+ pool_options = retrieve_pool_options(options)
+
+ if pool_options.empty?
+ Dalli::Client.new(addresses, options)
+ else
+ ensure_connection_pool_added!
+ ConnectionPool.new(pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) }
+ end
end
# Creates a new MemCacheStore object, with the given memcached server
@@ -92,28 +98,6 @@ module ActiveSupport
end
end
- # Reads multiple values from the cache using a single call to the
- # servers for all keys. Options can be passed in the last argument.
- def read_multi(*names)
- options = names.extract_options!
- options = merged_options(options)
-
- keys_to_names = Hash[names.map { |name| [normalize_key(name, options), name] }]
-
- raw_values = @data.get_multi(keys_to_names.keys)
- values = {}
-
- raw_values.each do |key, value|
- entry = deserialize_entry(value)
-
- unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
- values[keys_to_names[key]] = entry.value
- end
- end
-
- values
- end
-
# Increment a cached value. This method uses the memcached incr atomic
# operator and can only be used on values written with the :raw option.
# Calling it on a value not stored with :raw will initialize that value
@@ -122,7 +106,7 @@ module ActiveSupport
options = merged_options(options)
instrument(:increment, name, amount: amount) do
rescue_error_with nil do
- @data.incr(normalize_key(name, options), amount)
+ @data.with { |c| c.incr(normalize_key(name, options), amount, options[:expires_in]) }
end
end
end
@@ -135,7 +119,7 @@ module ActiveSupport
options = merged_options(options)
instrument(:decrement, name, amount: amount) do
rescue_error_with nil do
- @data.decr(normalize_key(name, options), amount)
+ @data.with { |c| c.decr(normalize_key(name, options), amount, options[:expires_in]) }
end
end
end
@@ -143,18 +127,18 @@ module ActiveSupport
# Clear the entire cache on all memcached servers. This method should
# be used with care when shared cache is being used.
def clear(options = nil)
- rescue_error_with(nil) { @data.flush_all }
+ rescue_error_with(nil) { @data.with { |c| c.flush_all } }
end
# Get the statistics from the memcached servers.
def stats
- @data.stats
+ @data.with { |c| c.stats }
end
private
# Read an entry from the cache.
def read_entry(key, options)
- rescue_error_with(nil) { deserialize_entry(@data.get(key, options)) }
+ rescue_error_with(nil) { deserialize_entry(@data.with { |c| c.get(key, options) }) }
end
# Write an entry to the cache.
@@ -167,13 +151,31 @@ module ActiveSupport
expires_in += 5.minutes
end
rescue_error_with false do
- @data.send(method, key, value, expires_in, options)
+ @data.with { |c| c.send(method, key, value, expires_in, options) }
+ end
+ end
+
+ # Reads multiple entries from the cache implementation.
+ def read_multi_entries(names, options)
+ keys_to_names = Hash[names.map { |name| [normalize_key(name, options), name] }]
+
+ raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) }
+ values = {}
+
+ raw_values.each do |key, value|
+ entry = deserialize_entry(value)
+
+ unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
+ values[keys_to_names[key]] = entry.value
+ end
end
+
+ values
end
# Delete an entry from the cache.
def delete_entry(key, options)
- rescue_error_with(false) { @data.delete(key) }
+ rescue_error_with(false) { @data.with { |c| c.delete(key) } }
end
# Memcache keys are binaries. So we need to force their encoding to binary
@@ -183,7 +185,7 @@ module ActiveSupport
key = super.dup
key = key.force_encoding(Encoding::ASCII_8BIT)
key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
- key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250
+ key = "#{key[0, 213]}:md5:#{ActiveSupport::Digest.hexdigest(key)}" if key.size > 250
key
end
diff --git a/activesupport/lib/active_support/cache/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb
new file mode 100644
index 0000000000..11c574258f
--- /dev/null
+++ b/activesupport/lib/active_support/cache/redis_cache_store.rb
@@ -0,0 +1,460 @@
+# frozen_string_literal: true
+
+begin
+ gem "redis", ">= 4.0.1"
+ require "redis"
+ require "redis/distributed"
+rescue LoadError
+ warn "The Redis cache store requires the redis gem, version 4.0.1 or later. Please add it to your Gemfile: `gem \"redis\", \"~> 4.0\"`"
+ raise
+end
+
+# Prefer the hiredis driver but don't require it.
+begin
+ require "redis/connection/hiredis"
+rescue LoadError
+end
+
+require "digest/sha2"
+require "active_support/core_ext/marshal"
+
+module ActiveSupport
+ module Cache
+ module ConnectionPoolLike
+ def with
+ yield self
+ end
+ end
+
+ ::Redis.include(ConnectionPoolLike)
+ ::Redis::Distributed.include(ConnectionPoolLike)
+
+ # Redis cache store.
+ #
+ # Deployment note: Take care to use a *dedicated Redis cache* rather
+ # than pointing this at your existing Redis server. It won't cope well
+ # with mixed usage patterns and it won't expire cache entries by default.
+ #
+ # Redis cache server setup guide: https://redis.io/topics/lru-cache
+ #
+ # * Supports vanilla Redis, hiredis, and Redis::Distributed.
+ # * Supports Memcached-like sharding across Redises with Redis::Distributed.
+ # * Fault tolerant. If the Redis server is unavailable, no exceptions are
+ # raised. Cache fetches are all misses and writes are dropped.
+ # * Local cache. Hot in-memory primary cache within block/middleware scope.
+ # * +read_multi+ and +write_multi+ support for Redis mget/mset. Use Redis::Distributed
+ # 4.0.1+ for distributed mget support.
+ # * +delete_matched+ support for Redis KEYS globs.
+ class RedisCacheStore < Store
+ # Keys are truncated with their own SHA2 digest if they exceed 1kB
+ MAX_KEY_BYTESIZE = 1024
+
+ DEFAULT_REDIS_OPTIONS = {
+ connect_timeout: 20,
+ read_timeout: 1,
+ write_timeout: 1,
+ reconnect_attempts: 0,
+ }
+
+ DEFAULT_ERROR_HANDLER = -> (method:, returning:, exception:) do
+ if logger
+ logger.error { "RedisCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.message}" }
+ end
+ end
+
+ # The maximum number of entries to receive per SCAN call.
+ SCAN_BATCH_SIZE = 1000
+ private_constant :SCAN_BATCH_SIZE
+
+ # Support raw values in the local cache strategy.
+ module LocalCacheWithRaw # :nodoc:
+ private
+ def read_entry(key, options)
+ entry = super
+ if options[:raw] && local_cache && entry
+ entry = deserialize_entry(entry.value)
+ end
+ entry
+ end
+
+ def write_entry(key, entry, options)
+ if options[:raw] && local_cache
+ raw_entry = Entry.new(serialize_entry(entry, raw: true))
+ raw_entry.expires_at = entry.expires_at
+ super(key, raw_entry, options)
+ else
+ super
+ end
+ end
+
+ def write_multi_entries(entries, options)
+ if options[:raw] && local_cache
+ raw_entries = entries.map do |key, entry|
+ raw_entry = Entry.new(serialize_entry(entry, raw: true))
+ raw_entry.expires_at = entry.expires_at
+ end.to_h
+
+ super(raw_entries, options)
+ else
+ super
+ end
+ end
+ end
+
+ prepend Strategy::LocalCache
+ prepend LocalCacheWithRaw
+
+ class << self
+ # Factory method to create a new Redis instance.
+ #
+ # Handles four options: :redis block, :redis instance, single :url
+ # string, and multiple :url strings.
+ #
+ # Option Class Result
+ # :redis Proc -> options[:redis].call
+ # :redis Object -> options[:redis]
+ # :url String -> Redis.new(url: …)
+ # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
+ #
+ def build_redis(redis: nil, url: nil, **redis_options) #:nodoc:
+ urls = Array(url)
+
+ if redis.is_a?(Proc)
+ redis.call
+ elsif redis
+ redis
+ elsif urls.size > 1
+ build_redis_distributed_client urls: urls, **redis_options
+ else
+ build_redis_client url: urls.first, **redis_options
+ end
+ end
+
+ private
+ def build_redis_distributed_client(urls:, **redis_options)
+ ::Redis::Distributed.new([], DEFAULT_REDIS_OPTIONS.merge(redis_options)).tap do |dist|
+ urls.each { |u| dist.add_node url: u }
+ end
+ end
+
+ def build_redis_client(url:, **redis_options)
+ ::Redis.new DEFAULT_REDIS_OPTIONS.merge(redis_options.merge(url: url))
+ end
+ end
+
+ attr_reader :redis_options
+ attr_reader :max_key_bytesize
+
+ # Creates a new Redis cache store.
+ #
+ # Handles three options: block provided to instantiate, single URL
+ # provided, and multiple URLs provided.
+ #
+ # :redis Proc -> options[:redis].call
+ # :url String -> Redis.new(url: …)
+ # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
+ #
+ # No namespace is set by default. Provide one if the Redis cache
+ # server is shared with other apps: <tt>namespace: 'myapp-cache'</tt>.
+ #
+ # Compression is enabled by default with a 1kB threshold, so cached
+ # values larger than 1kB are automatically compressed. Disable by
+ # passing <tt>compress: false</tt> or change the threshold by passing
+ # <tt>compress_threshold: 4.kilobytes</tt>.
+ #
+ # No expiry is set on cache entries by default. Redis is expected to
+ # be configured with an eviction policy that automatically deletes
+ # least-recently or -frequently used keys when it reaches max memory.
+ # See https://redis.io/topics/lru-cache for cache server setup.
+ #
+ # Race condition TTL is not set by default. This can be used to avoid
+ # "thundering herd" cache writes when hot cache entries are expired.
+ # See <tt>ActiveSupport::Cache::Store#fetch</tt> for more.
+ def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, expires_in: nil, race_condition_ttl: nil, error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
+ @redis_options = redis_options
+
+ @max_key_bytesize = MAX_KEY_BYTESIZE
+ @error_handler = error_handler
+
+ super namespace: namespace,
+ compress: compress, compress_threshold: compress_threshold,
+ expires_in: expires_in, race_condition_ttl: race_condition_ttl
+ end
+
+ def redis
+ @redis ||= begin
+ pool_options = self.class.send(:retrieve_pool_options, redis_options)
+
+ if pool_options.any?
+ self.class.send(:ensure_connection_pool_added!)
+ ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
+ else
+ self.class.build_redis(**redis_options)
+ end
+ end
+ end
+
+ def inspect
+ instance = @redis || @redis_options
+ "<##{self.class} options=#{options.inspect} redis=#{instance.inspect}>"
+ end
+
+ # Cache Store API implementation.
+ #
+ # Read multiple values at once. Returns a hash of requested keys ->
+ # fetched values.
+ def read_multi(*names)
+ if mget_capable?
+ instrument(:read_multi, names, options) do |payload|
+ read_multi_mget(*names).tap do |results|
+ payload[:hits] = results.keys
+ end
+ end
+ else
+ super
+ end
+ end
+
+ # Cache Store API implementation.
+ #
+ # Supports Redis KEYS glob patterns:
+ #
+ # h?llo matches hello, hallo and hxllo
+ # h*llo matches hllo and heeeello
+ # h[ae]llo matches hello and hallo, but not hillo
+ # h[^e]llo matches hallo, hbllo, ... but not hello
+ # h[a-b]llo matches hallo and hbllo
+ #
+ # Use \ to escape special characters if you want to match them verbatim.
+ #
+ # See https://redis.io/commands/KEYS for more.
+ #
+ # Failsafe: Raises errors.
+ def delete_matched(matcher, options = nil)
+ instrument :delete_matched, matcher do
+ unless String === matcher
+ raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
+ end
+ redis.with do |c|
+ pattern = namespace_key(matcher, options)
+ cursor = "0"
+ # Fetch keys in batches using SCAN to avoid blocking the Redis server.
+ begin
+ cursor, keys = c.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
+ c.del(*keys) unless keys.empty?
+ end until cursor == "0"
+ end
+ end
+ end
+
+ # Cache Store API implementation.
+ #
+ # Increment a cached value. This method uses the Redis incr atomic
+ # operator and can only be used on values written with the :raw option.
+ # Calling it on a value not stored with :raw will initialize that value
+ # to zero.
+ #
+ # Failsafe: Raises errors.
+ def increment(name, amount = 1, options = nil)
+ instrument :increment, name, amount: amount do
+ failsafe :increment do
+ redis.with { |c| c.incrby normalize_key(name, options), amount }
+ end
+ end
+ end
+
+ # Cache Store API implementation.
+ #
+ # Decrement a cached value. This method uses the Redis decr atomic
+ # operator and can only be used on values written with the :raw option.
+ # Calling it on a value not stored with :raw will initialize that value
+ # to zero.
+ #
+ # Failsafe: Raises errors.
+ def decrement(name, amount = 1, options = nil)
+ instrument :decrement, name, amount: amount do
+ failsafe :decrement do
+ redis.with { |c| c.decrby normalize_key(name, options), amount }
+ end
+ end
+ end
+
+ # Cache Store API implementation.
+ #
+ # Removes expired entries. Handled natively by Redis least-recently-/
+ # least-frequently-used expiry, so manual cleanup is not supported.
+ def cleanup(options = nil)
+ super
+ end
+
+ # Clear the entire cache on all Redis servers. Safe to use on
+ # shared servers if the cache is namespaced.
+ #
+ # Failsafe: Raises errors.
+ def clear(options = nil)
+ failsafe :clear do
+ if namespace = merged_options(options)[:namespace]
+ delete_matched "*", namespace: namespace
+ else
+ redis.with { |c| c.flushdb }
+ end
+ end
+ end
+
+ def mget_capable? #:nodoc:
+ set_redis_capabilities unless defined? @mget_capable
+ @mget_capable
+ end
+
+ def mset_capable? #:nodoc:
+ set_redis_capabilities unless defined? @mset_capable
+ @mset_capable
+ end
+
+ private
+ def set_redis_capabilities
+ case redis
+ when Redis::Distributed
+ @mget_capable = true
+ @mset_capable = false
+ else
+ @mget_capable = true
+ @mset_capable = true
+ end
+ end
+
+ # Store provider interface:
+ # Read an entry from the cache.
+ def read_entry(key, options = nil)
+ failsafe :read_entry do
+ deserialize_entry redis.with { |c| c.get(key) }
+ end
+ end
+
+ def read_multi_entries(names, _options)
+ if mget_capable?
+ read_multi_mget(*names)
+ else
+ super
+ end
+ end
+
+ def read_multi_mget(*names)
+ options = names.extract_options!
+ options = merged_options(options)
+
+ keys = names.map { |name| normalize_key(name, options) }
+
+ values = failsafe(:read_multi_mget, returning: {}) do
+ redis.with { |c| c.mget(*keys) }
+ end
+
+ names.zip(values).each_with_object({}) do |(name, value), results|
+ if value
+ entry = deserialize_entry(value)
+ unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
+ results[name] = entry.value
+ end
+ end
+ end
+ end
+
+ # Write an entry to the cache.
+ #
+ # Requires Redis 2.6.12+ for extended SET options.
+ def write_entry(key, entry, unless_exist: false, raw: false, expires_in: nil, race_condition_ttl: nil, **options)
+ serialized_entry = serialize_entry(entry, raw: raw)
+
+ # If race condition TTL is in use, ensure that cache entries
+ # stick around a bit longer after they would have expired
+ # so we can purposefully serve stale entries.
+ if race_condition_ttl && expires_in && expires_in > 0 && !raw
+ expires_in += 5.minutes
+ end
+
+ failsafe :write_entry, returning: false do
+ if unless_exist || expires_in
+ modifiers = {}
+ modifiers[:nx] = unless_exist
+ modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
+
+ redis.with { |c| c.set key, serialized_entry, modifiers }
+ else
+ redis.with { |c| c.set key, serialized_entry }
+ end
+ end
+ end
+
+ # Delete an entry from the cache.
+ def delete_entry(key, options)
+ failsafe :delete_entry, returning: false do
+ redis.with { |c| c.del key }
+ end
+ end
+
+ # Nonstandard store provider API to write multiple values at once.
+ def write_multi_entries(entries, expires_in: nil, **options)
+ if entries.any?
+ if mset_capable? && expires_in.nil?
+ failsafe :write_multi_entries do
+ redis.with { |c| c.mapped_mset(serialize_entries(entries, raw: options[:raw])) }
+ end
+ else
+ super
+ end
+ end
+ end
+
+ # Truncate keys that exceed 1kB.
+ def normalize_key(key, options)
+ truncate_key super.b
+ end
+
+ def truncate_key(key)
+ if key.bytesize > max_key_bytesize
+ suffix = ":sha2:#{::Digest::SHA2.hexdigest(key)}"
+ truncate_at = max_key_bytesize - suffix.bytesize
+ "#{key.byteslice(0, truncate_at)}#{suffix}"
+ else
+ key
+ end
+ end
+
+ def deserialize_entry(serialized_entry)
+ if serialized_entry
+ entry = Marshal.load(serialized_entry) rescue serialized_entry
+ entry.is_a?(Entry) ? entry : Entry.new(entry)
+ end
+ end
+
+ def serialize_entry(entry, raw: false)
+ if raw
+ entry.value.to_s
+ else
+ Marshal.dump(entry)
+ end
+ end
+
+ def serialize_entries(entries, raw: false)
+ entries.transform_values do |entry|
+ serialize_entry entry, raw: raw
+ end
+ end
+
+ def failsafe(method, returning: nil)
+ yield
+ rescue ::Redis::BaseConnectionError => e
+ handle_exception exception: e, method: method, returning: returning
+ returning
+ end
+
+ def handle_exception(exception:, method:, returning:)
+ if @error_handler
+ @error_handler.(method: method, exception: exception, returning: returning)
+ end
+ rescue => failsafe
+ warn "RedisCacheStore ignored exception in handle_exception: #{failsafe.class}: #{failsafe.message}\n #{failsafe.backtrace.join("\n ")}"
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb
index a3eef1190a..39b32fc7f6 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "../../core_ext/object/duplicable"
-require_relative "../../core_ext/string/inflections"
-require_relative "../../per_thread_registry"
+require "active_support/core_ext/object/duplicable"
+require "active_support/core_ext/string/inflections"
+require "active_support/per_thread_registry"
module ActiveSupport
module Cache
@@ -54,6 +54,17 @@ module ActiveSupport
@data[key]
end
+ def read_multi_entries(keys, options)
+ values = {}
+
+ keys.each do |name|
+ entry = read_entry(name, options)
+ values[name] = entry.value if entry
+ end
+
+ values
+ end
+
def write_entry(key, value, options)
@data[key] = value
true
@@ -116,6 +127,19 @@ module ActiveSupport
end
end
+ def read_multi_entries(keys, options)
+ return super unless local_cache
+
+ local_entries = local_cache.read_multi_entries(keys, options)
+ missed_keys = keys - local_entries.keys
+
+ if missed_keys.any?
+ local_entries.merge!(super(missed_keys, options))
+ else
+ local_entries
+ end
+ end
+
def write_entry(key, entry, options)
if options[:unless_exist]
local_cache.delete_entry(key, options) if local_cache
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index e1cbfc945c..a1b841ec3d 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
-require_relative "concern"
-require_relative "descendants_tracker"
-require_relative "core_ext/array/extract_options"
-require_relative "core_ext/class/attribute"
-require_relative "core_ext/kernel/reporting"
-require_relative "core_ext/kernel/singleton_class"
-require_relative "core_ext/string/filters"
-require_relative "deprecation"
+require "active_support/concern"
+require "active_support/descendants_tracker"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/class/attribute"
+require "active_support/core_ext/kernel/reporting"
+require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/string/filters"
+require "active_support/deprecation"
require "thread"
module ActiveSupport
@@ -298,8 +298,8 @@ module ActiveSupport
@kind = kind
@filter = filter
@key = compute_identifier filter
- @if = Array(options[:if])
- @unless = Array(options[:unless])
+ @if = check_conditionals(Array(options[:if]))
+ @unless = check_conditionals(Array(options[:unless]))
end
def filter; @key; end
@@ -323,7 +323,7 @@ module ActiveSupport
def duplicates?(other)
case @filter
- when Symbol, String
+ when Symbol
matches?(other.kind, other.filter)
else
false
@@ -350,9 +350,21 @@ module ActiveSupport
end
private
+ def check_conditionals(conditionals)
+ if conditionals.any? { |c| c.is_a?(String) }
+ raise ArgumentError, <<-MSG.squish
+ Passing string to be evaluated in :if and :unless conditional
+ options is not supported. Pass a symbol for an instance method,
+ or a lambda, proc or block, instead.
+ MSG
+ end
+
+ conditionals
+ end
+
def compute_identifier(filter)
case filter
- when String, ::Proc
+ when ::Proc
filter.object_id
else
filter
@@ -427,7 +439,6 @@ module ActiveSupport
# Filters support:
#
# Symbols:: A method to call.
- # Strings:: Some content to evaluate.
# Procs:: A proc to call with the object.
# Objects:: An object with a <tt>before_foo</tt> method on it to call.
#
@@ -437,8 +448,6 @@ module ActiveSupport
case filter
when Symbol
new(nil, filter, [], nil)
- when String
- new(nil, :instance_exec, [:value], compile_lambda(filter))
when Conditionals::Value
new(filter, :call, [:target, :value], nil)
when ::Proc
@@ -455,10 +464,6 @@ module ActiveSupport
new(filter, method_to_call, [:target], nil)
end
end
-
- def self.compile_lambda(filter)
- eval("lambda { |value| #{filter} }")
- end
end
# Execute before and after filters in a sequence instead of
@@ -651,26 +656,17 @@ module ActiveSupport
#
# ===== Options
#
- # * <tt>:if</tt> - A symbol, a string (deprecated) or an array of symbols,
- # each naming an instance method or a proc; the callback will be called
- # only when they all return a true value.
- # * <tt>:unless</tt> - A symbol, a string (deprecated) or an array of symbols,
- # each naming an instance method or a proc; the callback will be called
- # only when they all return a false value.
+ # * <tt>:if</tt> - A symbol or an array of symbols, each naming an instance
+ # method or a proc; the callback will be called only when they all return
+ # a true value.
+ # * <tt>:unless</tt> - A symbol or an array of symbols, each naming an
+ # instance method or a proc; the callback will be called only when they
+ # all return a false value.
# * <tt>:prepend</tt> - If +true+, the callback will be prepended to the
# existing chain rather than appended.
def set_callback(name, *filter_list, &block)
type, filters, options = normalize_callback_params(filter_list, block)
- if options[:if].is_a?(String) || options[:unless].is_a?(String)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing string to be evaluated in :if and :unless conditional
- options is deprecated and will be removed in Rails 5.2 without
- replacement. Pass a symbol for an instance method, or a lambda,
- proc or block, instead.
- MSG
- end
-
self_chain = get_callbacks name
mapped = filters.map do |filter|
Callback.build(self_chain, filter, type, options)
@@ -695,13 +691,6 @@ module ActiveSupport
def skip_callback(name, *filter_list, &block)
type, filters, options = normalize_callback_params(filter_list, block)
- if options[:if].is_a?(String) || options[:unless].is_a?(String)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing string to :if and :unless conditional options is deprecated
- and will be removed in Rails 5.2 without replacement.
- MSG
- end
-
options[:raise] = true unless options.key?(:raise)
__update_callbacks(name) do |target, chain|
@@ -760,8 +749,8 @@ module ActiveSupport
# * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after
# callbacks should be terminated by the <tt>:terminator</tt> option. By
# default after callbacks are executed no matter if callback chain was
- # terminated or not. This option makes sense only when <tt>:terminator</tt>
- # option is specified.
+ # terminated or not. This option has no effect if <tt>:terminator</tt>
+ # option is set to +nil+.
#
# * <tt>:scope</tt> - Indicates which methods should be executed when an
# object is used as a callback.
@@ -820,7 +809,9 @@ module ActiveSupport
names.each do |name|
name = name.to_sym
- set_callbacks name, CallbackChain.new(name, options)
+ ([self] + ActiveSupport::DescendantsTracker.descendants(self)).each do |target|
+ target.set_callbacks name, CallbackChain.new(name, options)
+ end
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def _run_#{name}_callbacks(&block)
diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb
index 32b5ca986b..b0a0d845e5 100644
--- a/activesupport/lib/active_support/concern.rb
+++ b/activesupport/lib/active_support/concern.rb
@@ -113,7 +113,7 @@ module ActiveSupport
def append_features(base)
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies) << self
- return false
+ false
else
return false if base < self
@_dependencies.each { |dep| base.include(dep) }
diff --git a/activesupport/lib/active_support/concurrency/load_interlock_aware_monitor.rb b/activesupport/lib/active_support/concurrency/load_interlock_aware_monitor.rb
new file mode 100644
index 0000000000..a8455c0048
--- /dev/null
+++ b/activesupport/lib/active_support/concurrency/load_interlock_aware_monitor.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require "monitor"
+
+module ActiveSupport
+ module Concurrency
+ # A monitor that will permit dependency loading while blocked waiting for
+ # the lock.
+ class LoadInterlockAwareMonitor < Monitor
+ # Enters an exclusive section, but allows dependency loading while blocked
+ def mon_enter
+ mon_try_enter ||
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads { super }
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb
index 8e8e27b4ec..4d6f7819bb 100644
--- a/activesupport/lib/active_support/configurable.rb
+++ b/activesupport/lib/active_support/configurable.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-require_relative "concern"
-require_relative "ordered_options"
-require_relative "core_ext/array/extract_options"
-require_relative "core_ext/regexp"
+require "active_support/concern"
+require "active_support/ordered_options"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/regexp"
module ActiveSupport
# Configurable provides a <tt>config</tt> method to store and retrieve
diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb
index fdc209ef68..6d83b76882 100644
--- a/activesupport/lib/active_support/core_ext/array.rb
+++ b/activesupport/lib/active_support/core_ext/array.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-require_relative "array/wrap"
-require_relative "array/access"
-require_relative "array/conversions"
-require_relative "array/extract_options"
-require_relative "array/grouping"
-require_relative "array/prepend_and_append"
-require_relative "array/inquiry"
+require "active_support/core_ext/array/wrap"
+require "active_support/core_ext/array/access"
+require "active_support/core_ext/array/conversions"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/array/grouping"
+require "active_support/core_ext/array/prepend_and_append"
+require "active_support/core_ext/array/inquiry"
diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb
index d67f99df0e..b7ff7a3907 100644
--- a/activesupport/lib/active_support/core_ext/array/access.rb
+++ b/activesupport/lib/active_support/core_ext/array/access.rb
@@ -35,8 +35,8 @@ class Array
# people.without "Aaron", "Todd"
# # => ["David", "Rafael"]
#
- # Note: This is an optimization of `Enumerable#without` that uses `Array#-`
- # instead of `Array#reject` for performance reasons.
+ # Note: This is an optimization of <tt>Enumerable#without</tt> that uses <tt>Array#-</tt>
+ # instead of <tt>Array#reject</tt> for performance reasons.
def without(*elements)
self - elements
end
diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb
index 6e17a81f0f..ea688ed2ea 100644
--- a/activesupport/lib/active_support/core_ext/array/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/array/conversions.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
-require_relative "../../xml_mini"
-require_relative "../hash/keys"
-require_relative "../string/inflections"
-require_relative "../object/to_param"
-require_relative "../object/to_query"
+require "active_support/xml_mini"
+require "active_support/core_ext/hash/keys"
+require "active_support/core_ext/string/inflections"
+require "active_support/core_ext/object/to_param"
+require "active_support/core_ext/object/to_query"
class Array
# Converts the array to a comma-separated sentence where the last element is
@@ -181,7 +181,7 @@ class Array
# </messages>
#
def to_xml(options = {})
- require_relative "../../builder" unless defined?(Builder)
+ require "active_support/builder" unless defined?(Builder)
options = options.dup
options[:indent] ||= 2
diff --git a/activesupport/lib/active_support/core_ext/array/inquiry.rb b/activesupport/lib/active_support/core_ext/array/inquiry.rb
index 4f4a9fa361..92c61bf201 100644
--- a/activesupport/lib/active_support/core_ext/array/inquiry.rb
+++ b/activesupport/lib/active_support/core_ext/array/inquiry.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../array_inquirer"
+require "active_support/array_inquirer"
class Array
# Wraps the array in an +ArrayInquirer+ object, which gives a friendlier way
diff --git a/activesupport/lib/active_support/core_ext/big_decimal.rb b/activesupport/lib/active_support/core_ext/big_decimal.rb
index 5568db689b..9e6a9d6331 100644
--- a/activesupport/lib/active_support/core_ext/big_decimal.rb
+++ b/activesupport/lib/active_support/core_ext/big_decimal.rb
@@ -1,3 +1,3 @@
# frozen_string_literal: true
-require_relative "big_decimal/conversions"
+require "active_support/core_ext/big_decimal/conversions"
diff --git a/activesupport/lib/active_support/core_ext/class.rb b/activesupport/lib/active_support/core_ext/class.rb
index 65e1d68399..1c110fd07b 100644
--- a/activesupport/lib/active_support/core_ext/class.rb
+++ b/activesupport/lib/active_support/core_ext/class.rb
@@ -1,4 +1,4 @@
# frozen_string_literal: true
-require_relative "class/attribute"
-require_relative "class/subclasses"
+require "active_support/core_ext/class/attribute"
+require "active_support/core_ext/class/subclasses"
diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb
index 3453fc4ac6..fa33ff945f 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "../kernel/singleton_class"
-require_relative "../module/redefine_method"
-require_relative "../array/extract_options"
+require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/module/redefine_method"
+require "active_support/core_ext/array/extract_options"
class Class
# Declare a class-level attribute whose value is inheritable by subclasses.
@@ -98,7 +98,7 @@ class Class
singleton_class.silence_redefinition_of_method("#{name}?")
define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate
- ivar = "@#{name}"
+ ivar = "@#{name}".to_sym
singleton_class.silence_redefinition_of_method("#{name}=")
define_singleton_method("#{name}=") do |val|
diff --git a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
index b4dbcc6698..a77354e153 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
@@ -3,4 +3,4 @@
# cattr_* became mattr_* aliases in 7dfbd91b0780fbd6a1dd9bfbc176e10894871d2d,
# but we keep this around for libraries that directly require it knowing they
# want cattr_*. No need to deprecate.
-require_relative "../module/attribute_accessors"
+require "active_support/core_ext/module/attribute_accessors"
diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb
index 4c910feb44..75e65337b7 100644
--- a/activesupport/lib/active_support/core_ext/class/subclasses.rb
+++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb
@@ -1,8 +1,5 @@
# frozen_string_literal: true
-require_relative "../module/anonymous"
-require_relative "../module/reachable"
-
class Class
begin
# Test if this Ruby supports each_object against singleton_class
diff --git a/activesupport/lib/active_support/core_ext/date.rb b/activesupport/lib/active_support/core_ext/date.rb
index 2a2ed5496f..cce73f2db2 100644
--- a/activesupport/lib/active_support/core_ext/date.rb
+++ b/activesupport/lib/active_support/core_ext/date.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "date/acts_like"
-require_relative "date/blank"
-require_relative "date/calculations"
-require_relative "date/conversions"
-require_relative "date/zones"
+require "active_support/core_ext/date/acts_like"
+require "active_support/core_ext/date/blank"
+require "active_support/core_ext/date/calculations"
+require "active_support/core_ext/date/conversions"
+require "active_support/core_ext/date/zones"
diff --git a/activesupport/lib/active_support/core_ext/date/acts_like.rb b/activesupport/lib/active_support/core_ext/date/acts_like.rb
index 81769129b5..c8077f3774 100644
--- a/activesupport/lib/active_support/core_ext/date/acts_like.rb
+++ b/activesupport/lib/active_support/core_ext/date/acts_like.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../object/acts_like"
+require "active_support/core_ext/object/acts_like"
class Date
# Duck-types as a Date-like class. See Object#acts_like?.
diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb
index 1a19ee8b1e..1cd7acb05d 100644
--- a/activesupport/lib/active_support/core_ext/date/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date/calculations.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
require "date"
-require_relative "../../duration"
-require_relative "../object/acts_like"
-require_relative "zones"
-require_relative "../time/zones"
-require_relative "../date_and_time/calculations"
+require "active_support/duration"
+require "active_support/core_ext/object/acts_like"
+require "active_support/core_ext/date/zones"
+require "active_support/core_ext/time/zones"
+require "active_support/core_ext/date_and_time/calculations"
class Date
include DateAndTime::Calculations
diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb
index 88fb572725..870119dc7f 100644
--- a/activesupport/lib/active_support/core_ext/date/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date/conversions.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
require "date"
-require_relative "../../inflector/methods"
-require_relative "zones"
-require_relative "../module/redefine_method"
+require "active_support/inflector/methods"
+require "active_support/core_ext/date/zones"
+require "active_support/core_ext/module/redefine_method"
class Date
DATE_FORMATS = {
diff --git a/activesupport/lib/active_support/core_ext/date/zones.rb b/activesupport/lib/active_support/core_ext/date/zones.rb
index 1aed070df0..2dcf97cff8 100644
--- a/activesupport/lib/active_support/core_ext/date/zones.rb
+++ b/activesupport/lib/active_support/core_ext/date/zones.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "date"
-require_relative "../date_and_time/zones"
+require "active_support/core_ext/date_and_time/zones"
class Date
include DateAndTime::Zones
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 8546f7e57b..de13f00e60 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../object/try"
+require "active_support/core_ext/object/try"
module DateAndTime
module Calculations
@@ -20,9 +20,9 @@ module DateAndTime
advance(days: -1)
end
- # Returns a new date/time representing the previous day.
- def prev_day
- advance(days: -1)
+ # Returns a new date/time the specified number of days ago.
+ def prev_day(days = 1)
+ advance(days: -days)
end
# Returns a new date/time representing tomorrow.
@@ -30,9 +30,9 @@ module DateAndTime
advance(days: 1)
end
- # Returns a new date/time representing the next day.
- def next_day
- advance(days: 1)
+ # Returns a new date/time the specified number of days in the future.
+ def next_day(days = 1)
+ advance(days: days)
end
# Returns true if the date/time is today.
@@ -60,6 +60,16 @@ module DateAndTime
!WEEKEND_DAYS.include?(wday)
end
+ # Returns true if the date/time before <tt>date_or_time</tt>.
+ def before?(date_or_time)
+ self < date_or_time
+ end
+
+ # Returns true if the date/time after <tt>date_or_time</tt>.
+ def after?(date_or_time)
+ self > date_or_time
+ end
+
# Returns a new date/time the specified number of days ago.
def days_ago(days)
advance(days: -days)
@@ -188,9 +198,9 @@ module DateAndTime
end
end
- # Short-hand for months_since(1).
- def next_month
- months_since(1)
+ # Returns a new date/time the specified number of months in the future.
+ def next_month(months = 1)
+ advance(months: months)
end
# Short-hand for months_since(3)
@@ -198,9 +208,9 @@ module DateAndTime
months_since(3)
end
- # Short-hand for years_since(1).
- def next_year
- years_since(1)
+ # Returns a new date/time the specified number of years in the future.
+ def next_year(years = 1)
+ advance(years: years)
end
# Returns a new date/time representing the given day in the previous week.
@@ -223,11 +233,15 @@ module DateAndTime
end
alias_method :last_weekday, :prev_weekday
+ # Returns a new date/time the specified number of months ago.
+ def prev_month(months = 1)
+ advance(months: -months)
+ end
+
# Short-hand for months_ago(1).
- def prev_month
+ def last_month
months_ago(1)
end
- alias_method :last_month, :prev_month
# Short-hand for months_ago(3).
def prev_quarter
@@ -235,11 +249,15 @@ module DateAndTime
end
alias_method :last_quarter, :prev_quarter
+ # Returns a new date/time the specified number of years ago.
+ def prev_year(years = 1)
+ advance(years: -years)
+ end
+
# Short-hand for years_ago(1).
- def prev_year
+ def last_year
years_ago(1)
end
- alias_method :last_year, :prev_year
# Returns the number of days to the start of the week on the given day.
# Week is assumed to start on +start_day+, default is
@@ -322,20 +340,28 @@ module DateAndTime
beginning_of_year..end_of_year
end
- # Returns specific next occurring day of week
+ # Returns a new date/time representing the next occurrence of the specified day of week.
+ #
+ # today = Date.today # => Thu, 14 Dec 2017
+ # today.next_occurring(:monday) # => Mon, 18 Dec 2017
+ # today.next_occurring(:thursday) # => Thu, 21 Dec 2017
def next_occurring(day_of_week)
current_day_number = wday != 0 ? wday - 1 : 6
from_now = DAYS_INTO_WEEK.fetch(day_of_week) - current_day_number
from_now += 7 unless from_now > 0
- since(from_now.days)
+ advance(days: from_now)
end
- # Returns specific previous occurring day of week
+ # Returns a new date/time representing the previous occurrence of the specified day of week.
+ #
+ # today = Date.today # => Thu, 14 Dec 2017
+ # today.prev_occurring(:monday) # => Mon, 11 Dec 2017
+ # today.prev_occurring(:thursday) # => Thu, 07 Dec 2017
def prev_occurring(day_of_week)
current_day_number = wday != 0 ? wday - 1 : 6
ago = current_day_number - DAYS_INTO_WEEK.fetch(day_of_week)
ago += 7 unless ago > 0
- ago(ago.days)
+ advance(days: -ago)
end
private
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
index 5b3aabf6a5..d33c36ef73 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../module/attribute_accessors"
+require "active_support/core_ext/module/attribute_accessors"
module DateAndTime
module Compatibility
diff --git a/activesupport/lib/active_support/core_ext/date_time.rb b/activesupport/lib/active_support/core_ext/date_time.rb
index b3d6773e4f..790dbeec1b 100644
--- a/activesupport/lib/active_support/core_ext/date_time.rb
+++ b/activesupport/lib/active_support/core_ext/date_time.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "date_time/acts_like"
-require_relative "date_time/blank"
-require_relative "date_time/calculations"
-require_relative "date_time/compatibility"
-require_relative "date_time/conversions"
+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/compatibility"
+require "active_support/core_ext/date_time/conversions"
diff --git a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
index 02acb5fe21..5dccdfe219 100644
--- a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "date"
-require_relative "../object/acts_like"
+require "active_support/core_ext/object/acts_like"
class DateTime
# Duck-types as a Date-like class. See Object#acts_like?.
diff --git a/activesupport/lib/active_support/core_ext/date_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb
index bb9714545e..7600a067cc 100644
--- a/activesupport/lib/active_support/core_ext/date_time/compatibility.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb
@@ -1,16 +1,16 @@
# frozen_string_literal: true
-require_relative "../date_and_time/compatibility"
-require_relative "../module/redefine_method"
+require "active_support/core_ext/date_and_time/compatibility"
+require "active_support/core_ext/module/redefine_method"
class DateTime
include DateAndTime::Compatibility
silence_redefinition_of_method :to_time
- # Either return an instance of `Time` with the same UTC offset
- # as +self+ or an instance of `Time` representing the same time
- # in the the local system timezone depending on the setting of
+ # Either return an instance of +Time+ with the same UTC offset
+ # as +self+ or an instance of +Time+ representing the same time
+ # in the local system timezone depending on the setting of
# on the setting of +ActiveSupport.to_time_preserves_timezone+.
def to_time
preserve_timezone ? getlocal(utc_offset) : getlocal
diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb
index e4c8f9898d..29725c89f7 100644
--- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
require "date"
-require_relative "../../inflector/methods"
-require_relative "../time/conversions"
-require_relative "calculations"
-require_relative "../../values/time_zone"
+require "active_support/inflector/methods"
+require "active_support/core_ext/time/conversions"
+require "active_support/core_ext/date_time/calculations"
+require "active_support/values/time_zone"
class DateTime
# Convert to a formatted string. See Time::DATE_FORMATS for predefined formats.
diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb
index 17733d955c..edde4f46b9 100644
--- a/activesupport/lib/active_support/core_ext/enumerable.rb
+++ b/activesupport/lib/active_support/core_ext/enumerable.rb
@@ -3,50 +3,37 @@
module Enumerable
# Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements
# when we omit an identity.
+
+ # We can't use Refinements here because Refinements with Module which will be prepended
+ # doesn't work well https://bugs.ruby-lang.org/issues/13446
+ alias :_original_sum_with_required_identity :sum # :nodoc:
+ private :_original_sum_with_required_identity
+
+ # Calculates a sum from the elements.
#
- # We tried shimming it to attempt the fast native method, rescue TypeError,
- # and fall back to the compatible implementation, but that's much slower than
- # just calling the compat method in the first place.
- if Enumerable.instance_methods(false).include?(:sum) && !((?a..?b).sum rescue false)
- # We can't use Refinements here because Refinements with Module which will be prepended
- # doesn't work well https://bugs.ruby-lang.org/issues/13446
- alias :_original_sum_with_required_identity :sum
- private :_original_sum_with_required_identity
- # Calculates a sum from the elements.
- #
- # payments.sum { |p| p.price * p.tax_rate }
- # payments.sum(&:price)
- #
- # The latter is a shortcut for:
- #
- # payments.inject(0) { |sum, p| sum + p.price }
- #
- # It can also calculate the sum without the use of a block.
- #
- # [5, 15, 10].sum # => 30
- # ['foo', 'bar'].sum # => "foobar"
- # [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5]
- #
- # The default sum of an empty list is zero. You can override this default:
- #
- # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0)
- def sum(identity = nil, &block)
- if identity
- _original_sum_with_required_identity(identity, &block)
- elsif block_given?
- map(&block).sum(identity)
- else
- inject(:+) || 0
- end
- end
- else
- def sum(identity = nil, &block)
- if block_given?
- map(&block).sum(identity)
- else
- sum = identity ? inject(identity, :+) : inject(:+)
- sum || identity || 0
- end
+ # payments.sum { |p| p.price * p.tax_rate }
+ # payments.sum(&:price)
+ #
+ # The latter is a shortcut for:
+ #
+ # payments.inject(0) { |sum, p| sum + p.price }
+ #
+ # It can also calculate the sum without the use of a block.
+ #
+ # [5, 15, 10].sum # => 30
+ # ['foo', 'bar'].sum # => "foobar"
+ # [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5]
+ #
+ # The default sum of an empty list is zero. You can override this default:
+ #
+ # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0)
+ def sum(identity = nil, &block)
+ if identity
+ _original_sum_with_required_identity(identity, &block)
+ elsif block_given?
+ map(&block).sum(identity)
+ else
+ inject(:+) || 0
end
end
@@ -133,27 +120,21 @@ class Range #:nodoc:
end
end
-# Array#sum was added in Ruby 2.4 but it only works with Numeric elements.
-#
-# We tried shimming it to attempt the fast native method, rescue TypeError,
-# and fall back to the compatible implementation, but that's much slower than
-# just calling the compat method in the first place.
-if Array.instance_methods(false).include?(:sum) && !(%w[a].sum rescue false)
- # Using Refinements here in order not to expose our internal method
- using Module.new {
- refine Array do
- alias :orig_sum :sum
- end
- }
+# Using Refinements here in order not to expose our internal method
+using Module.new {
+ refine Array do
+ alias :orig_sum :sum
+ end
+}
- class Array
- def sum(init = nil, &block) #:nodoc:
- if init.is_a?(Numeric) || first.is_a?(Numeric)
- init ||= 0
- orig_sum(init, &block)
- else
- super
- end
+class Array #:nodoc:
+ # Array#sum was added in Ruby 2.4 but it only works with Numeric elements.
+ def sum(init = nil, &block)
+ if init.is_a?(Numeric) || first.is_a?(Numeric)
+ init ||= 0
+ orig_sum(init, &block)
+ else
+ super
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/file.rb b/activesupport/lib/active_support/core_ext/file.rb
index 3c2364167d..64553bfa4e 100644
--- a/activesupport/lib/active_support/core_ext/file.rb
+++ b/activesupport/lib/active_support/core_ext/file.rb
@@ -1,3 +1,3 @@
# frozen_string_literal: true
-require_relative "file/atomic"
+require "active_support/core_ext/file/atomic"
diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb
index a74a8c15a6..c4b9e5f1a0 100644
--- a/activesupport/lib/active_support/core_ext/hash.rb
+++ b/activesupport/lib/active_support/core_ext/hash.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require_relative "hash/compact"
-require_relative "hash/conversions"
-require_relative "hash/deep_merge"
-require_relative "hash/except"
-require_relative "hash/indifferent_access"
-require_relative "hash/keys"
-require_relative "hash/reverse_merge"
-require_relative "hash/slice"
-require_relative "hash/transform_values"
+require "active_support/core_ext/hash/conversions"
+require "active_support/core_ext/hash/deep_merge"
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/hash/keys"
+require "active_support/core_ext/hash/reverse_merge"
+require "active_support/core_ext/hash/slice"
diff --git a/activesupport/lib/active_support/core_ext/hash/compact.rb b/activesupport/lib/active_support/core_ext/hash/compact.rb
index d6364dd9f3..28c8d86b9b 100644
--- a/activesupport/lib/active_support/core_ext/hash/compact.rb
+++ b/activesupport/lib/active_support/core_ext/hash/compact.rb
@@ -1,29 +1,5 @@
# frozen_string_literal: true
-class Hash
- unless Hash.instance_methods(false).include?(:compact)
- # Returns a hash with non +nil+ values.
- #
- # hash = { a: true, b: false, c: nil }
- # hash.compact # => { a: true, b: false }
- # hash # => { a: true, b: false, c: nil }
- # { c: nil }.compact # => {}
- # { c: true }.compact # => { c: true }
- def compact
- select { |_, value| !value.nil? }
- end
- end
+require "active_support/deprecation"
- unless Hash.instance_methods(false).include?(:compact!)
- # Replaces current hash with non +nil+ values.
- # Returns +nil+ if no changes were made, otherwise returns the hash.
- #
- # hash = { a: true, b: false, c: nil }
- # hash.compact! # => { a: true, b: false }
- # hash # => { a: true, b: false }
- # { c: true }.compact! # => nil
- def compact!
- reject! { |_, value| value.nil? }
- end
- end
-end
+ActiveSupport::Deprecation.warn "Ruby 2.4+ (required by Rails 6) provides Hash#compact and Hash#compact! natively, so requiring active_support/core_ext/hash/compact is no longer necessary. Requiring it will raise LoadError in Rails 6.1."
diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb
index 0c0701134e..5b48254646 100644
--- a/activesupport/lib/active_support/core_ext/hash/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
-require_relative "../../xml_mini"
-require_relative "../../time"
-require_relative "../object/blank"
-require_relative "../object/to_param"
-require_relative "../object/to_query"
-require_relative "../array/wrap"
-require_relative "reverse_merge"
-require_relative "../string/inflections"
+require "active_support/xml_mini"
+require "active_support/time"
+require "active_support/core_ext/object/blank"
+require "active_support/core_ext/object/to_param"
+require "active_support/core_ext/object/to_query"
+require "active_support/core_ext/array/wrap"
+require "active_support/core_ext/hash/reverse_merge"
+require "active_support/core_ext/string/inflections"
class Hash
# Returns a string containing an XML representation of its receiver:
@@ -73,7 +73,7 @@ class Hash
# configure your own builder with the <tt>:builder</tt> option. The method also accepts
# options like <tt>:dasherize</tt> and friends, they are forwarded to the builder.
def to_xml(options = {})
- require_relative "../../builder" unless defined?(Builder)
+ require "active_support/builder" unless defined?(Builder)
options = options.dup
options[:indent] ||= 2
@@ -165,7 +165,7 @@ module ActiveSupport
Hash[params.map { |k, v| [k.to_s.tr("-", "_"), normalize_keys(v)] } ]
when Array
params.map { |v| normalize_keys(v) }
- else
+ else
params
end
end
@@ -178,7 +178,7 @@ module ActiveSupport
process_array(value)
when String
value
- else
+ else
raise "can't typecast #{value.class.name} - #{value.inspect}"
end
end
diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
index 4681d986db..a38f33f128 100644
--- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
+++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../hash_with_indifferent_access"
+require "active_support/hash_with_indifferent_access"
class Hash
# Returns an <tt>ActiveSupport::HashWithIndifferentAccess</tt> out of its receiver:
diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb
index 94dc225edb..2bd0a56ea4 100644
--- a/activesupport/lib/active_support/core_ext/hash/slice.rb
+++ b/activesupport/lib/active_support/core_ext/hash/slice.rb
@@ -21,9 +21,8 @@ class Hash
# valid_keys = [:mass, :velocity, :time]
# search(options.slice(*valid_keys))
def slice(*keys)
- keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
- keys.each_with_object(self.class.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
- end
+ keys.each_with_object(Hash.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
+ end unless method_defined?(:slice)
# Replaces the hash with only the given keys.
# Returns a hash containing the removed key/value pairs.
@@ -31,7 +30,6 @@ class Hash
# { a: 1, b: 2, c: 3, d: 4 }.slice!(:a, :b)
# # => {:c=>3, :d=>4}
def slice!(*keys)
- keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
omit = slice(*self.keys - keys)
hash = slice(*keys)
hash.default = default
diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
index 4b19c9fc1f..fc15130c9e 100644
--- a/activesupport/lib/active_support/core_ext/hash/transform_values.rb
+++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
@@ -1,32 +1,5 @@
# frozen_string_literal: true
-class Hash
- # Returns a new hash with the results of running +block+ once for every value.
- # The keys are unchanged.
- #
- # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } # => { a: 2, b: 4, c: 6 }
- #
- # If you do not provide a +block+, it will return an Enumerator
- # for chaining with other methods:
- #
- # { a: 1, b: 2 }.transform_values.with_index { |v, i| [v, i].join.to_i } # => { a: 10, b: 21 }
- def transform_values
- return enum_for(:transform_values) { size } unless block_given?
- return {} if empty?
- result = self.class.new
- each do |key, value|
- result[key] = yield(value)
- end
- result
- end unless method_defined? :transform_values
+require "active_support/deprecation"
- # Destructively converts all values using the +block+ operations.
- # Same as +transform_values+ but modifies +self+.
- def transform_values!
- return enum_for(:transform_values!) { size } unless block_given?
- each do |key, value|
- self[key] = yield(value)
- end
- end unless method_defined? :transform_values!
- # TODO: Remove this file when supporting only Ruby 2.4+.
-end
+ActiveSupport::Deprecation.warn "Ruby 2.4+ (required by Rails 6) provides Hash#transform_values natively, so requiring active_support/core_ext/hash/transform_values is no longer necessary. Requiring it will raise LoadError in Rails 6.1."
diff --git a/activesupport/lib/active_support/core_ext/integer.rb b/activesupport/lib/active_support/core_ext/integer.rb
index df80e7ffdb..d22701306a 100644
--- a/activesupport/lib/active_support/core_ext/integer.rb
+++ b/activesupport/lib/active_support/core_ext/integer.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-require_relative "integer/multiple"
-require_relative "integer/inflections"
-require_relative "integer/time"
+require "active_support/core_ext/integer/multiple"
+require "active_support/core_ext/integer/inflections"
+require "active_support/core_ext/integer/time"
diff --git a/activesupport/lib/active_support/core_ext/integer/inflections.rb b/activesupport/lib/active_support/core_ext/integer/inflections.rb
index 7192e92346..aef3266f28 100644
--- a/activesupport/lib/active_support/core_ext/integer/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/integer/inflections.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../inflector"
+require "active_support/inflector"
class Integer
# Ordinalize turns a number into an ordinal string used to denote the
diff --git a/activesupport/lib/active_support/core_ext/integer/time.rb b/activesupport/lib/active_support/core_ext/integer/time.rb
index 30c8f3bcf2..5efb89cf9f 100644
--- a/activesupport/lib/active_support/core_ext/integer/time.rb
+++ b/activesupport/lib/active_support/core_ext/integer/time.rb
@@ -1,29 +1,20 @@
# frozen_string_literal: true
-require_relative "../../duration"
-require_relative "../numeric/time"
+require "active_support/duration"
+require "active_support/core_ext/numeric/time"
class Integer
- # Enables the use of time calculations and declarations, like <tt>45.minutes +
- # 2.hours + 4.years</tt>.
+ # Returns a Duration instance matching the number of months provided.
#
- # These methods use Time#advance for precise date calculations when using
- # <tt>from_now</tt>, +ago+, etc. as well as adding or subtracting their
- # results from a Time object.
- #
- # # equivalent to Time.now.advance(months: 1)
- # 1.month.from_now
- #
- # # equivalent to Time.now.advance(years: 2)
- # 2.years.from_now
- #
- # # equivalent to Time.now.advance(months: 4, years: 5)
- # (4.months + 5.years).from_now
+ # 2.months # => 2 months
def months
ActiveSupport::Duration.months(self)
end
alias :month :months
+ # Returns a Duration instance matching the number of years provided.
+ #
+ # 2.years # => 2 years
def years
ActiveSupport::Duration.years(self)
end
diff --git a/activesupport/lib/active_support/core_ext/kernel.rb b/activesupport/lib/active_support/core_ext/kernel.rb
index 30810ce315..0f4356fbdd 100644
--- a/activesupport/lib/active_support/core_ext/kernel.rb
+++ b/activesupport/lib/active_support/core_ext/kernel.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "kernel/agnostics"
-require_relative "kernel/concern"
-require_relative "kernel/reporting"
-require_relative "kernel/singleton_class"
+require "active_support/core_ext/kernel/agnostics"
+require "active_support/core_ext/kernel/concern"
+require "active_support/core_ext/kernel/reporting"
+require "active_support/core_ext/kernel/singleton_class"
diff --git a/activesupport/lib/active_support/core_ext/kernel/concern.rb b/activesupport/lib/active_support/core_ext/kernel/concern.rb
index 32af9981a5..0b2baed780 100644
--- a/activesupport/lib/active_support/core_ext/kernel/concern.rb
+++ b/activesupport/lib/active_support/core_ext/kernel/concern.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../module/concerning"
+require "active_support/core_ext/module/concerning"
module Kernel
module_function
diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb
index 8445643730..d91e3fba6a 100644
--- a/activesupport/lib/active_support/core_ext/module.rb
+++ b/activesupport/lib/active_support/core_ext/module.rb
@@ -1,14 +1,14 @@
# frozen_string_literal: true
-require_relative "module/aliasing"
-require_relative "module/introspection"
-require_relative "module/anonymous"
-require_relative "module/reachable"
-require_relative "module/attribute_accessors"
-require_relative "module/attribute_accessors_per_thread"
-require_relative "module/attr_internal"
-require_relative "module/concerning"
-require_relative "module/delegation"
-require_relative "module/deprecation"
-require_relative "module/redefine_method"
-require_relative "module/remove_method"
+require "active_support/core_ext/module/aliasing"
+require "active_support/core_ext/module/introspection"
+require "active_support/core_ext/module/anonymous"
+require "active_support/core_ext/module/reachable"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/module/attribute_accessors_per_thread"
+require "active_support/core_ext/module/attr_internal"
+require "active_support/core_ext/module/concerning"
+require "active_support/core_ext/module/delegation"
+require "active_support/core_ext/module/deprecation"
+require "active_support/core_ext/module/redefine_method"
+require "active_support/core_ext/module/remove_method"
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 e667c1f4a4..01fee0fb74 100644
--- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../array/extract_options"
-require_relative "../regexp"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/regexp"
# Extends the module object with class/module and instance accessors for
# class/module attributes, just like the native attr* accessors for instance
@@ -163,10 +163,10 @@ class Module
# parent class. Similarly if parent class changes the value then that would
# change the value of subclasses too.
#
- # class Male < Person
+ # class Citizen < Person
# end
#
- # Male.new.hair_colors << :blue
+ # Citizen.new.hair_colors << :blue
# Person.new.hair_colors # => [:brown, :black, :blonde, :red, :blue]
#
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
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 90a350c5dc..4b9b6ea9bd 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
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../array/extract_options"
-require_relative "../regexp"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/regexp"
# Extends the module object with class/module and instance accessors for
# class/module attributes, just like the native attr* accessors for instance
diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb
index 17c202a24a..7bbbf321ab 100644
--- a/activesupport/lib/active_support/core_ext/module/concerning.rb
+++ b/activesupport/lib/active_support/core_ext/module/concerning.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../concern"
+require "active_support/concern"
class Module
# = Bite-sized separation of concerns
@@ -22,7 +22,7 @@ class Module
#
# == Using comments:
#
- # class Todo
+ # class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
@@ -30,7 +30,6 @@ class Module
# has_many :events
#
# before_create :track_creation
- # after_destroy :track_deletion
#
# private
# def track_creation
@@ -42,7 +41,7 @@ class Module
#
# Noisy syntax.
#
- # class Todo
+ # class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
@@ -52,7 +51,6 @@ class Module
# included do
# has_many :events
# before_create :track_creation
- # after_destroy :track_deletion
# end
#
# private
@@ -70,7 +68,7 @@ class Module
# increased overhead can be a reasonable tradeoff even if it reduces our
# at-a-glance perception of how things work.
#
- # class Todo
+ # class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
@@ -82,7 +80,7 @@ class Module
# By quieting the mix-in noise, we arrive at a natural, low-ceremony way to
# separate bite-sized concerns.
#
- # class Todo
+ # class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
@@ -90,7 +88,6 @@ class Module
# included do
# has_many :events
# before_create :track_creation
- # after_destroy :track_deletion
# end
#
# private
@@ -101,7 +98,7 @@ class Module
# end
#
# Todo.ancestors
- # # => [Todo, Todo::EventTracking, Object]
+ # # => [Todo, Todo::EventTracking, ApplicationRecord, Object]
#
# This small step has some wonderful ripple effects. We can
# * grok the behavior of our class in one glance,
diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index 1840dc942f..7f42f44efb 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "set"
-require_relative "../regexp"
+require "active_support/core_ext/regexp"
class Module
# Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
@@ -22,8 +22,9 @@ class Module
# ==== Options
# * <tt>:to</tt> - Specifies the target object
# * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix
- # * <tt>:allow_nil</tt> - if set to true, prevents a +Module::DelegationError+
+ # * <tt>:allow_nil</tt> - If set to true, prevents a +Module::DelegationError+
# from being raised
+ # * <tt>:private</tt> - If set to true, changes method visibility to private
#
# The macro receives one or more method names (specified as symbols or
# strings) and the name of the target object via the <tt>:to</tt> option
@@ -114,12 +115,26 @@ class Module
# invoice.customer_name # => 'John Doe'
# invoice.customer_address # => 'Vimmersvej 13'
#
+ # The delegated methods are public by default.
+ # Pass <tt>private: true</tt> to change that.
+ #
+ # class User < ActiveRecord::Base
+ # has_one :profile
+ # delegate :first_name, to: :profile
+ # delegate :date_of_birth, to: :profile, private: true
+ #
+ # def age
+ # Date.today.year - date_of_birth.year
+ # end
+ # end
+ #
+ # User.new.first_name # => "Tomas"
+ # User.new.date_of_birth # => NoMethodError: private method `date_of_birth' called for #<User:0x00000008221340>
+ # User.new.age # => 2
+ #
# If the target is +nil+ and does not respond to the delegated method a
- # +Module::DelegationError+ is raised, as with any other value. Sometimes,
- # however, it makes sense to be robust to that situation and that is the
- # purpose of the <tt>:allow_nil</tt> option: If the target is not +nil+, or it
- # is and responds to the method, everything works as usual. But if it is +nil+
- # and does not respond to the delegated method, +nil+ is returned.
+ # +Module::DelegationError+ is raised. If you wish to instead return +nil+,
+ # use the <tt>:allow_nil</tt> option.
#
# class User < ActiveRecord::Base
# has_one :profile
@@ -154,7 +169,7 @@ class Module
# Foo.new("Bar").name # raises NoMethodError: undefined method `name'
#
# The target method must be public, otherwise it will raise +NoMethodError+.
- def delegate(*methods, to: nil, prefix: nil, allow_nil: nil)
+ def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil)
unless to
raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter)."
end
@@ -176,7 +191,7 @@ class Module
to = to.to_s
to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)
- methods.map do |method|
+ method_names = methods.map do |method|
# Attribute writer methods only accept one argument. Makes sure []=
# methods still accept two arguments.
definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"
@@ -216,6 +231,9 @@ class Module
module_eval(method_def, file, line)
end
+
+ private(*method_names) if private
+ method_names
end
# When building decorators, a common pattern may emerge:
diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb
index 540385ef6f..c5bb598bd1 100644
--- a/activesupport/lib/active_support/core_ext/module/introspection.rb
+++ b/activesupport/lib/active_support/core_ext/module/introspection.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../inflector"
+require "active_support/inflector"
class Module
# Returns the name of the module containing this one.
diff --git a/activesupport/lib/active_support/core_ext/module/reachable.rb b/activesupport/lib/active_support/core_ext/module/reachable.rb
index 91b230b46c..e9cbda5245 100644
--- a/activesupport/lib/active_support/core_ext/module/reachable.rb
+++ b/activesupport/lib/active_support/core_ext/module/reachable.rb
@@ -1,10 +1,11 @@
# frozen_string_literal: true
-require_relative "anonymous"
-require_relative "../string/inflections"
+require "active_support/core_ext/module/anonymous"
+require "active_support/core_ext/string/inflections"
class Module
def reachable? #:nodoc:
!anonymous? && name.safe_constantize.equal?(self)
end
+ deprecate :reachable?
end
diff --git a/activesupport/lib/active_support/core_ext/module/redefine_method.rb b/activesupport/lib/active_support/core_ext/module/redefine_method.rb
index a0a6622ca4..5bd8e6e973 100644
--- a/activesupport/lib/active_support/core_ext/module/redefine_method.rb
+++ b/activesupport/lib/active_support/core_ext/module/redefine_method.rb
@@ -1,23 +1,14 @@
# frozen_string_literal: true
class Module
- if RUBY_VERSION >= "2.3"
- # Marks the named method as intended to be redefined, if it exists.
- # Suppresses the Ruby method redefinition warning. Prefer
- # #redefine_method where possible.
- def silence_redefinition_of_method(method)
- if method_defined?(method) || private_method_defined?(method)
- # This suppresses the "method redefined" warning; the self-alias
- # looks odd, but means we don't need to generate a unique name
- alias_method method, method
- end
- end
- else
- def silence_redefinition_of_method(method)
- if method_defined?(method) || private_method_defined?(method)
- alias_method :__rails_redefine, method
- remove_method :__rails_redefine
- end
+ # Marks the named method as intended to be redefined, if it exists.
+ # Suppresses the Ruby method redefinition warning. Prefer
+ # #redefine_method where possible.
+ def silence_redefinition_of_method(method)
+ if method_defined?(method) || private_method_defined?(method)
+ # This suppresses the "method redefined" warning; the self-alias
+ # looks odd, but means we don't need to generate a unique name
+ alias_method method, method
end
end
diff --git a/activesupport/lib/active_support/core_ext/module/remove_method.rb b/activesupport/lib/active_support/core_ext/module/remove_method.rb
index 704f144bdd..97eb5f9eca 100644
--- a/activesupport/lib/active_support/core_ext/module/remove_method.rb
+++ b/activesupport/lib/active_support/core_ext/module/remove_method.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "redefine_method"
+require "active_support/core_ext/module/redefine_method"
class Module
# Removes the named method, if it exists.
diff --git a/activesupport/lib/active_support/core_ext/name_error.rb b/activesupport/lib/active_support/core_ext/name_error.rb
index d4f1e01140..6d37cd9dfd 100644
--- a/activesupport/lib/active_support/core_ext/name_error.rb
+++ b/activesupport/lib/active_support/core_ext/name_error.rb
@@ -10,6 +10,11 @@ class NameError
# end
# # => "HelloWorld"
def missing_name
+ # Since ruby v2.3.0 `did_you_mean` gem is loaded by default.
+ # It extends NameError#message with spell corrections which are SLOW.
+ # We should use original_message message instead.
+ message = respond_to?(:original_message) ? original_message : self.message
+
if /undefined local variable or method/ !~ message
$1 if /((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/ =~ message
end
diff --git a/activesupport/lib/active_support/core_ext/numeric.rb b/activesupport/lib/active_support/core_ext/numeric.rb
index 76e33a7cb0..fe778470f1 100644
--- a/activesupport/lib/active_support/core_ext/numeric.rb
+++ b/activesupport/lib/active_support/core_ext/numeric.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
-require_relative "numeric/bytes"
-require_relative "numeric/time"
-require_relative "numeric/inquiry"
-require_relative "numeric/conversions"
+require "active_support/core_ext/numeric/bytes"
+require "active_support/core_ext/numeric/time"
+require "active_support/core_ext/numeric/conversions"
diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
index 05528f5069..7fcd0d0311 100644
--- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "../big_decimal/conversions"
-require_relative "../../number_helper"
-require_relative "../module/deprecation"
+require "active_support/core_ext/big_decimal/conversions"
+require "active_support/number_helper"
+require "active_support/core_ext/module/deprecation"
module ActiveSupport::NumericWithFormat
# Provides options for converting numbers into formatted strings.
@@ -108,19 +108,19 @@ module ActiveSupport::NumericWithFormat
when Integer, String
super(format)
when :phone
- return ActiveSupport::NumberHelper.number_to_phone(self, options || {})
+ ActiveSupport::NumberHelper.number_to_phone(self, options || {})
when :currency
- return ActiveSupport::NumberHelper.number_to_currency(self, options || {})
+ ActiveSupport::NumberHelper.number_to_currency(self, options || {})
when :percentage
- return ActiveSupport::NumberHelper.number_to_percentage(self, options || {})
+ ActiveSupport::NumberHelper.number_to_percentage(self, options || {})
when :delimited
- return ActiveSupport::NumberHelper.number_to_delimited(self, options || {})
+ ActiveSupport::NumberHelper.number_to_delimited(self, options || {})
when :rounded
- return ActiveSupport::NumberHelper.number_to_rounded(self, options || {})
+ ActiveSupport::NumberHelper.number_to_rounded(self, options || {})
when :human
- return ActiveSupport::NumberHelper.number_to_human(self, options || {})
+ ActiveSupport::NumberHelper.number_to_human(self, options || {})
when :human_size
- return ActiveSupport::NumberHelper.number_to_human_size(self, options || {})
+ ActiveSupport::NumberHelper.number_to_human_size(self, options || {})
when Symbol
super()
else
@@ -129,12 +129,6 @@ module ActiveSupport::NumericWithFormat
end
end
-# Ruby 2.4+ unifies Fixnum & Bignum into Integer.
-if 0.class == Integer
- Integer.prepend ActiveSupport::NumericWithFormat
-else
- Fixnum.prepend ActiveSupport::NumericWithFormat
- Bignum.prepend ActiveSupport::NumericWithFormat
-end
+Integer.prepend ActiveSupport::NumericWithFormat
Float.prepend ActiveSupport::NumericWithFormat
BigDecimal.prepend ActiveSupport::NumericWithFormat
diff --git a/activesupport/lib/active_support/core_ext/numeric/inquiry.rb b/activesupport/lib/active_support/core_ext/numeric/inquiry.rb
index 15334c91f1..e05e40825b 100644
--- a/activesupport/lib/active_support/core_ext/numeric/inquiry.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/inquiry.rb
@@ -1,28 +1,5 @@
# frozen_string_literal: true
-unless 1.respond_to?(:positive?) # TODO: Remove this file when we drop support to ruby < 2.3
- class Numeric
- # Returns true if the number is positive.
- #
- # 1.positive? # => true
- # 0.positive? # => false
- # -1.positive? # => false
- def positive?
- self > 0
- end
+require "active_support/deprecation"
- # Returns true if the number is negative.
- #
- # -1.negative? # => true
- # 0.negative? # => false
- # 1.negative? # => false
- def negative?
- self < 0
- end
- end
-
- class Complex
- undef :positive?
- undef :negative?
- end
-end
+ActiveSupport::Deprecation.warn "Ruby 2.4+ (required by Rails 6) provides Numeric#positive? and Numeric#negative? natively, so requiring active_support/core_ext/numeric/inquiry is no longer necessary. Requiring it will raise LoadError in Rails 6.1."
diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb
index 0cee87cb86..bc4627f7a2 100644
--- a/activesupport/lib/active_support/core_ext/numeric/time.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/time.rb
@@ -1,25 +1,15 @@
# frozen_string_literal: true
-require_relative "../../duration"
-require_relative "../time/calculations"
-require_relative "../time/acts_like"
-require_relative "../date/calculations"
-require_relative "../date/acts_like"
+require "active_support/duration"
+require "active_support/core_ext/time/calculations"
+require "active_support/core_ext/time/acts_like"
+require "active_support/core_ext/date/calculations"
+require "active_support/core_ext/date/acts_like"
class Numeric
- # Enables the use of time calculations and declarations, like 45.minutes + 2.hours + 4.years.
+ # Returns a Duration instance matching the number of seconds provided.
#
- # These methods use Time#advance for precise date calculations when using from_now, ago, etc.
- # as well as adding or subtracting their results from a Time object. For example:
- #
- # # equivalent to Time.current.advance(months: 1)
- # 1.month.from_now
- #
- # # equivalent to Time.current.advance(years: 2)
- # 2.years.from_now
- #
- # # equivalent to Time.current.advance(months: 4, years: 5)
- # (4.months + 5.years).from_now
+ # 2.seconds # => 2 seconds
def seconds
ActiveSupport::Duration.seconds(self)
end
@@ -66,10 +56,10 @@ class Numeric
alias :fortnight :fortnights
# Returns the number of milliseconds equivalent to the seconds provided.
- # Used with the standard time durations, like 1.hour.in_milliseconds --
- # so we can feed them to JavaScript functions like getTime().
+ # Used with the standard time durations.
#
- # 2.in_milliseconds # => 2_000
+ # 2.in_milliseconds # => 2000
+ # 1.hour.in_milliseconds # => 3600000
def in_milliseconds
self * 1000
end
diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb
index 23f5eec8c7..efd34cc692 100644
--- a/activesupport/lib/active_support/core_ext/object.rb
+++ b/activesupport/lib/active_support/core_ext/object.rb
@@ -1,16 +1,16 @@
# frozen_string_literal: true
-require_relative "object/acts_like"
-require_relative "object/blank"
-require_relative "object/duplicable"
-require_relative "object/deep_dup"
-require_relative "object/try"
-require_relative "object/inclusion"
+require "active_support/core_ext/object/acts_like"
+require "active_support/core_ext/object/blank"
+require "active_support/core_ext/object/duplicable"
+require "active_support/core_ext/object/deep_dup"
+require "active_support/core_ext/object/try"
+require "active_support/core_ext/object/inclusion"
-require_relative "object/conversions"
-require_relative "object/instance_variables"
+require "active_support/core_ext/object/conversions"
+require "active_support/core_ext/object/instance_variables"
-require_relative "object/json"
-require_relative "object/to_param"
-require_relative "object/to_query"
-require_relative "object/with_options"
+require "active_support/core_ext/object/json"
+require "active_support/core_ext/object/to_param"
+require "active_support/core_ext/object/to_query"
+require "active_support/core_ext/object/with_options"
diff --git a/activesupport/lib/active_support/core_ext/object/acts_like.rb b/activesupport/lib/active_support/core_ext/object/acts_like.rb
index 2eb72f6b3a..403ee20e39 100644
--- a/activesupport/lib/active_support/core_ext/object/acts_like.rb
+++ b/activesupport/lib/active_support/core_ext/object/acts_like.rb
@@ -7,6 +7,15 @@ class Object
# <tt>x.acts_like?(:date)</tt> to do duck-type-safe comparisons, since classes that
# we want to act like Time simply need to define an <tt>acts_like_time?</tt> method.
def acts_like?(duck)
- respond_to? :"acts_like_#{duck}?"
+ case duck
+ when :time
+ respond_to? :acts_like_time?
+ when :date
+ respond_to? :acts_like_date?
+ when :string
+ respond_to? :acts_like_string?
+ else
+ respond_to? :"acts_like_#{duck}?"
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb
index 397adbdb5a..2ca431ab10 100644
--- a/activesupport/lib/active_support/core_ext/object/blank.rb
+++ b/activesupport/lib/active_support/core_ext/object/blank.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-require_relative "../regexp"
+require "active_support/core_ext/regexp"
+require "concurrent/map"
class Object
# An object is blank if it's false, empty, or a whitespace string.
@@ -102,6 +103,9 @@ end
class String
BLANK_RE = /\A[[:space:]]*\z/
+ ENCODED_BLANKS = Concurrent::Map.new do |h, enc|
+ h[enc] = Regexp.new(BLANK_RE.source.encode(enc), BLANK_RE.options | Regexp::FIXEDENCODING)
+ end
# A string is blank if it's empty or contains whitespaces only:
#
@@ -119,7 +123,12 @@ class String
# The regexp that matches blank strings is expensive. For the case of empty
# strings we can speed up this method (~3.5x) with an empty? call. The
# penalty for the rest of strings is marginal.
- empty? || BLANK_RE.match?(self)
+ empty? ||
+ begin
+ BLANK_RE.match?(self)
+ rescue Encoding::CompatibilityError
+ ENCODED_BLANKS[self.encoding].match?(self)
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/conversions.rb b/activesupport/lib/active_support/core_ext/object/conversions.rb
index fdc154188a..624fb8d77c 100644
--- a/activesupport/lib/active_support/core_ext/object/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/object/conversions.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "to_param"
-require_relative "to_query"
-require_relative "../array/conversions"
-require_relative "../hash/conversions"
+require "active_support/core_ext/object/to_param"
+require "active_support/core_ext/object/to_query"
+require "active_support/core_ext/array/conversions"
+require "active_support/core_ext/hash/conversions"
diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb
index 4021b15de6..c66c5eb2d9 100644
--- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb
+++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "duplicable"
+require "active_support/core_ext/object/duplicable"
class Object
# Returns a deep copy of object if it's duplicable. If it's
diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb
index 1744a44df6..c78ee6bbfc 100644
--- a/activesupport/lib/active_support/core_ext/object/duplicable.rb
+++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb
@@ -75,8 +75,11 @@ end
class Symbol
begin
- :symbol.dup # Ruby 2.4.x.
- "symbol_from_string".to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0.
+ :symbol.dup
+
+ # Some symbols couldn't be duped in Ruby 2.4.0 only, due to a bug.
+ # This feature check catches any regression.
+ "symbol_from_string".to_sym.dup
rescue TypeError
# Symbols are not duplicable:
@@ -108,8 +111,8 @@ require "bigdecimal"
class BigDecimal
# BigDecimals are duplicable:
#
- # BigDecimal.new("1.2").duplicable? # => true
- # BigDecimal.new("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)>
+ # BigDecimal("1.2").duplicable? # => true
+ # BigDecimal("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)>
def duplicable?
true
end
diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb
index 30495313d2..416059d17b 100644
--- a/activesupport/lib/active_support/core_ext/object/json.rb
+++ b/activesupport/lib/active_support/core_ext/object/json.rb
@@ -5,15 +5,16 @@ require "json"
require "bigdecimal"
require "uri/generic"
require "pathname"
-require_relative "../big_decimal/conversions" # for #to_s
-require_relative "../hash/except"
-require_relative "../hash/slice"
-require_relative "instance_variables"
+require "active_support/core_ext/big_decimal/conversions" # for #to_s
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/slice"
+require "active_support/core_ext/object/instance_variables"
require "time"
-require_relative "../time/conversions"
-require_relative "../date_time/conversions"
-require_relative "../date/conversions"
+require "active_support/core_ext/time/conversions"
+require "active_support/core_ext/date_time/conversions"
+require "active_support/core_ext/date/conversions"
+#--
# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
# their default behavior. That said, we need to define the basic to_json method in all of them,
# otherwise they will always use to_json gem implementation, which is backwards incompatible in
@@ -135,6 +136,12 @@ module Enumerable
end
end
+class IO
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
class Range
def as_json(options = nil) #:nodoc:
to_s
diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb
index c57488bcbc..6d2bdd70f3 100644
--- a/activesupport/lib/active_support/core_ext/object/to_param.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_param.rb
@@ -1,3 +1,3 @@
# frozen_string_literal: true
-require_relative "to_query"
+require "active_support/core_ext/object/to_query"
diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb
index 47766f6012..2838fd76be 100644
--- a/activesupport/lib/active_support/core_ext/object/with_options.rb
+++ b/activesupport/lib/active_support/core_ext/object/with_options.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../option_merger"
+require "active_support/option_merger"
class Object
# An elegant way to factor duplication out of options passed to a series of
@@ -62,7 +62,7 @@ class Object
#
# validates :content, length: { minimum: 50 }, if: -> { content.present? }
#
- # Hence the inherited default for `if` key is ignored.
+ # Hence the inherited default for +if+ key is ignored.
#
# NOTE: You cannot call class methods implicitly inside of with_options.
# You can access these methods using the class name instead:
diff --git a/activesupport/lib/active_support/core_ext/range.rb b/activesupport/lib/active_support/core_ext/range.rb
index 89bbbfcb81..4074e91d17 100644
--- a/activesupport/lib/active_support/core_ext/range.rb
+++ b/activesupport/lib/active_support/core_ext/range.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-require_relative "range/conversions"
-require_relative "range/include_range"
-require_relative "range/overlaps"
-require_relative "range/each"
+require "active_support/core_ext/range/conversions"
+require "active_support/core_ext/range/include_range"
+require "active_support/core_ext/range/include_time_with_zone"
+require "active_support/core_ext/range/overlaps"
+require "active_support/core_ext/range/each"
diff --git a/activesupport/lib/active_support/core_ext/range/conversions.rb b/activesupport/lib/active_support/core_ext/range/conversions.rb
index 37868f5875..8832fbcb3c 100644
--- a/activesupport/lib/active_support/core_ext/range/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/range/conversions.rb
@@ -2,7 +2,13 @@
module ActiveSupport::RangeWithFormat
RANGE_FORMATS = {
- db: Proc.new { |start, stop| "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" }
+ db: -> (start, stop) do
+ case start
+ when String then "BETWEEN '#{start}' AND '#{stop}'"
+ else
+ "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'"
+ end
+ end
}
# Convert range to a formatted string. See RANGE_FORMATS for predefined formats.
diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb
index cdff6393d7..2f22cd0e92 100644
--- a/activesupport/lib/active_support/core_ext/range/each.rb
+++ b/activesupport/lib/active_support/core_ext/range/each.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/time_with_zone"
+
module ActiveSupport
module EachTimeWithZone #:nodoc:
def each(&block)
@@ -15,7 +17,7 @@ module ActiveSupport
private
def ensure_iteration_allowed
- raise TypeError, "can't iterate from #{first.class}" if first.is_a?(Time)
+ raise TypeError, "can't iterate from #{first.class}" if first.is_a?(TimeWithZone)
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/range/include_time_with_zone.rb b/activesupport/lib/active_support/core_ext/range/include_time_with_zone.rb
new file mode 100644
index 0000000000..5f80acf68e
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/range/include_time_with_zone.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "active_support/time_with_zone"
+
+module ActiveSupport
+ module IncludeTimeWithZone #:nodoc:
+ # Extends the default Range#include? to support ActiveSupport::TimeWithZone.
+ #
+ # (1.hour.ago..1.hour.from_now).include?(Time.current) # => true
+ #
+ def include?(value)
+ if first.is_a?(TimeWithZone)
+ cover?(value)
+ elsif last.is_a?(TimeWithZone)
+ cover?(value)
+ else
+ super
+ end
+ end
+ end
+end
+
+Range.prepend(ActiveSupport::IncludeTimeWithZone)
diff --git a/activesupport/lib/active_support/core_ext/regexp.rb b/activesupport/lib/active_support/core_ext/regexp.rb
index efbd708aee..d92943c7ae 100644
--- a/activesupport/lib/active_support/core_ext/regexp.rb
+++ b/activesupport/lib/active_support/core_ext/regexp.rb
@@ -4,8 +4,4 @@ class Regexp #:nodoc:
def multiline?
options & MULTILINE == MULTILINE
end
-
- def match?(string, pos = 0)
- !!match(string, pos)
- end unless //.respond_to?(:match?)
end
diff --git a/activesupport/lib/active_support/core_ext/string.rb b/activesupport/lib/active_support/core_ext/string.rb
index 491eec2fc9..757d15c51a 100644
--- a/activesupport/lib/active_support/core_ext/string.rb
+++ b/activesupport/lib/active_support/core_ext/string.rb
@@ -1,15 +1,15 @@
# frozen_string_literal: true
-require_relative "string/conversions"
-require_relative "string/filters"
-require_relative "string/multibyte"
-require_relative "string/starts_ends_with"
-require_relative "string/inflections"
-require_relative "string/access"
-require_relative "string/behavior"
-require_relative "string/output_safety"
-require_relative "string/exclude"
-require_relative "string/strip"
-require_relative "string/inquiry"
-require_relative "string/indent"
-require_relative "string/zones"
+require "active_support/core_ext/string/conversions"
+require "active_support/core_ext/string/filters"
+require "active_support/core_ext/string/multibyte"
+require "active_support/core_ext/string/starts_ends_with"
+require "active_support/core_ext/string/inflections"
+require "active_support/core_ext/string/access"
+require "active_support/core_ext/string/behavior"
+require "active_support/core_ext/string/output_safety"
+require "active_support/core_ext/string/exclude"
+require "active_support/core_ext/string/strip"
+require "active_support/core_ext/string/inquiry"
+require "active_support/core_ext/string/indent"
+require "active_support/core_ext/string/zones"
diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb
index f8f6524b2b..29a88b07ad 100644
--- a/activesupport/lib/active_support/core_ext/string/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/string/conversions.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "date"
-require_relative "../time/calculations"
+require "active_support/core_ext/time/calculations"
class String
# Converts a string to a Time value.
diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb
index 66e721eea3..df0e79afa8 100644
--- a/activesupport/lib/active_support/core_ext/string/filters.rb
+++ b/activesupport/lib/active_support/core_ext/string/filters.rb
@@ -78,6 +78,47 @@ class String
"#{self[0, stop]}#{omission}"
end
+ # Truncates +text+ to at most <tt>bytesize</tt> bytes in length without
+ # breaking string encoding by splitting multibyte characters or breaking
+ # grapheme clusters ("perceptual characters") by truncating at combining
+ # characters.
+ #
+ # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".size
+ # => 20
+ # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".bytesize
+ # => 80
+ # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".truncate_bytes(20)
+ # => "🔪🔪🔪🔪…"
+ #
+ # The truncated text ends with the <tt>:omission</tt> string, defaulting
+ # to "…", for a total length not exceeding <tt>bytesize</tt>.
+ def truncate_bytes(truncate_at, omission: "…")
+ omission ||= ""
+
+ case
+ when bytesize <= truncate_at
+ dup
+ when omission.bytesize > truncate_at
+ raise ArgumentError, "Omission #{omission.inspect} is #{omission.bytesize}, larger than the truncation length of #{truncate_at} bytes"
+ when omission.bytesize == truncate_at
+ omission.dup
+ else
+ self.class.new.tap do |cut|
+ cut_at = truncate_at - omission.bytesize
+
+ scan(/\X/) do |grapheme|
+ if cut.bytesize + grapheme.bytesize <= cut_at
+ cut << grapheme
+ else
+ break
+ end
+ end
+
+ cut << omission
+ end
+ end
+ end
+
# Truncates a given +text+ after a given number of words (<tt>words_count</tt>):
#
# 'Once upon a time in a world far far away'.truncate_words(4)
diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb
index b5bb385033..8af301734a 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../../inflector/methods"
-require_relative "../../inflector/transliterate"
+require "active_support/inflector/methods"
+require "active_support/inflector/transliterate"
# String inflections define new methods on the String class to transform names for different purposes.
# For instance, you can figure out the name of a table from the name of a class.
@@ -174,7 +174,7 @@ class String
# <%= link_to(@person.name, person_path) %>
# # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
#
- # To preserve the case of the characters in a string, use the `preserve_case` argument.
+ # To preserve the case of the characters in a string, use the +preserve_case+ argument.
#
# class Person
# def to_param
diff --git a/activesupport/lib/active_support/core_ext/string/inquiry.rb b/activesupport/lib/active_support/core_ext/string/inquiry.rb
index 92069981b6..a796d5fb4f 100644
--- a/activesupport/lib/active_support/core_ext/string/inquiry.rb
+++ b/activesupport/lib/active_support/core_ext/string/inquiry.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../string_inquirer"
+require "active_support/string_inquirer"
class String
# Wraps the current string in the <tt>ActiveSupport::StringInquirer</tt> class,
diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb
index fba5b166a2..6cceb46507 100644
--- a/activesupport/lib/active_support/core_ext/string/multibyte.rb
+++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../multibyte"
+require "active_support/multibyte"
class String
# == Multibyte proxy
@@ -11,11 +11,14 @@ class String
# encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy
# class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string.
#
- # >> "lj".upcase
- # => "lj"
# >> "lj".mb_chars.upcase.to_s
# => "LJ"
#
+ # NOTE: Ruby 2.4 and later support native Unicode case mappings:
+ #
+ # >> "lj".upcase
+ # => "LJ"
+ #
# == Method chaining
#
# All the methods on the Chars proxy which normally return a string will return a Chars object. This allows
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 600b41da10..f3bdc2977e 100644
--- a/activesupport/lib/active_support/core_ext/string/output_safety.rb
+++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
require "erb"
-require_relative "../kernel/singleton_class"
-require_relative "../module/redefine_method"
-require_relative "../../multibyte/unicode"
+require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/module/redefine_method"
+require "active_support/multibyte/unicode"
class ERB
module Util
@@ -15,9 +15,6 @@ class ERB
# A utility method for escaping HTML tag characters.
# This method is also aliased as <tt>h</tt>.
#
- # In your ERB templates, use this method to escape any unsafe content. For example:
- # <%= h @person.name %>
- #
# puts html_escape('is a > 0 & a < 10?')
# # => is a &gt; 0 &amp; a &lt; 10?
def html_escape(s)
@@ -253,7 +250,7 @@ class String
# Marks a string as trusted safe. It will be inserted into HTML with no
# 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
+ # +raw+ helper in views. It is recommended that you use +sanitize+ instead of
# this method. It should never be called on user input.
def html_safe
ActiveSupport::SafeBuffer.new(self)
diff --git a/activesupport/lib/active_support/core_ext/string/strip.rb b/activesupport/lib/active_support/core_ext/string/strip.rb
index cc26274e4a..6f9834bb16 100644
--- a/activesupport/lib/active_support/core_ext/string/strip.rb
+++ b/activesupport/lib/active_support/core_ext/string/strip.rb
@@ -20,6 +20,8 @@ class String
# Technically, it looks for the least indented non-empty line
# in the whole string, and removes that amount of leading whitespace.
def strip_heredoc
- gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "".freeze)
+ gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "".freeze).tap do |stripped|
+ stripped.freeze if frozen?
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/string/zones.rb b/activesupport/lib/active_support/core_ext/string/zones.rb
index db30c03a8e..55dc231464 100644
--- a/activesupport/lib/active_support/core_ext/string/zones.rb
+++ b/activesupport/lib/active_support/core_ext/string/zones.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "conversions"
-require_relative "../time/zones"
+require "active_support/core_ext/string/conversions"
+require "active_support/core_ext/time/zones"
class String
# Converts String to a TimeWithZone in the current zone if Time.zone or Time.zone_default
diff --git a/activesupport/lib/active_support/core_ext/time.rb b/activesupport/lib/active_support/core_ext/time.rb
index 4e16274443..c809def05f 100644
--- a/activesupport/lib/active_support/core_ext/time.rb
+++ b/activesupport/lib/active_support/core_ext/time.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "time/acts_like"
-require_relative "time/calculations"
-require_relative "time/compatibility"
-require_relative "time/conversions"
-require_relative "time/zones"
+require "active_support/core_ext/time/acts_like"
+require "active_support/core_ext/time/calculations"
+require "active_support/core_ext/time/compatibility"
+require "active_support/core_ext/time/conversions"
+require "active_support/core_ext/time/zones"
diff --git a/activesupport/lib/active_support/core_ext/time/acts_like.rb b/activesupport/lib/active_support/core_ext/time/acts_like.rb
index 309418df42..8572b49639 100644
--- a/activesupport/lib/active_support/core_ext/time/acts_like.rb
+++ b/activesupport/lib/active_support/core_ext/time/acts_like.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../object/acts_like"
+require "active_support/core_ext/object/acts_like"
class Time
# Duck-types as a Time-like class. See Object#acts_like?.
diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb
index 0e51df44ef..120768dec5 100644
--- a/activesupport/lib/active_support/core_ext/time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/time/calculations.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
-require_relative "../../duration"
-require_relative "conversions"
-require_relative "../../time_with_zone"
-require_relative "zones"
-require_relative "../date_and_time/calculations"
-require_relative "../date/calculations"
+require "active_support/duration"
+require "active_support/core_ext/time/conversions"
+require "active_support/time_with_zone"
+require "active_support/core_ext/time/zones"
+require "active_support/core_ext/date_and_time/calculations"
+require "active_support/core_ext/date/calculations"
class Time
include DateAndTime::Calculations
diff --git a/activesupport/lib/active_support/core_ext/time/compatibility.rb b/activesupport/lib/active_support/core_ext/time/compatibility.rb
index 147ae0f802..495e4f307b 100644
--- a/activesupport/lib/active_support/core_ext/time/compatibility.rb
+++ b/activesupport/lib/active_support/core_ext/time/compatibility.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../date_and_time/compatibility"
-require_relative "../module/redefine_method"
+require "active_support/core_ext/date_and_time/compatibility"
+require "active_support/core_ext/module/redefine_method"
class Time
include DateAndTime::Compatibility
diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb
index e3fc930ef6..345cb2832c 100644
--- a/activesupport/lib/active_support/core_ext/time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/time/conversions.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../../inflector/methods"
-require_relative "../../values/time_zone"
+require "active_support/inflector/methods"
+require "active_support/values/time_zone"
class Time
DATE_FORMATS = {
diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb
index c48edb135f..a5588fd488 100644
--- a/activesupport/lib/active_support/core_ext/time/zones.rb
+++ b/activesupport/lib/active_support/core_ext/time/zones.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "../../time_with_zone"
-require_relative "acts_like"
-require_relative "../date_and_time/zones"
+require "active_support/time_with_zone"
+require "active_support/core_ext/time/acts_like"
+require "active_support/core_ext/date_and_time/zones"
class Time
include DateAndTime::Zones
@@ -55,10 +55,10 @@ class Time
# end
# end
#
- # NOTE: This won't affect any <tt>ActiveSupport::TimeWithZone</tt>
- # objects that have already been created, e.g. any model timestamp
- # attributes that have been read before the block will remain in
- # the application's default timezone.
+ # NOTE: This won't affect any <tt>ActiveSupport::TimeWithZone</tt>
+ # objects that have already been created, e.g. any model timestamp
+ # attributes that have been read before the block will remain in
+ # the application's default timezone.
def use_zone(time_zone)
new_zone = find_zone!(time_zone)
begin
diff --git a/activesupport/lib/active_support/core_ext/uri.rb b/activesupport/lib/active_support/core_ext/uri.rb
index c93c0b5c2d..cdd81ae562 100644
--- a/activesupport/lib/active_support/core_ext/uri.rb
+++ b/activesupport/lib/active_support/core_ext/uri.rb
@@ -1,10 +1,8 @@
# frozen_string_literal: true
require "uri"
-str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese.
-parser = URI::Parser.new
-unless str == parser.unescape(parser.escape(str))
+if RUBY_VERSION < "2.6.0"
require "active_support/core_ext/module/redefine_method"
URI::Parser.class_eval do
silence_redefinition_of_method :unescape
@@ -13,7 +11,7 @@ unless str == parser.unescape(parser.escape(str))
# YK: My initial experiments say yes, but let's be sure please
enc = str.encoding
enc = Encoding::UTF_8 if enc == Encoding::US_ASCII
- str.gsub(escaped) { |match| [match[1, 2].hex].pack("C") }.force_encoding(enc)
+ str.dup.force_encoding(Encoding::ASCII_8BIT).gsub(escaped) { |match| [match[1, 2].hex].pack("C") }.force_encoding(enc)
end
end
end
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index ad2f21205f..0f59558bb5 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -4,17 +4,17 @@ require "set"
require "thread"
require "concurrent/map"
require "pathname"
-require_relative "core_ext/module/aliasing"
-require_relative "core_ext/module/attribute_accessors"
-require_relative "core_ext/module/introspection"
-require_relative "core_ext/module/anonymous"
-require_relative "core_ext/object/blank"
-require_relative "core_ext/kernel/reporting"
-require_relative "core_ext/load_error"
-require_relative "core_ext/name_error"
-require_relative "core_ext/string/starts_ends_with"
-require_relative "dependencies/interlock"
-require_relative "inflector"
+require "active_support/core_ext/module/aliasing"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/module/introspection"
+require "active_support/core_ext/module/anonymous"
+require "active_support/core_ext/object/blank"
+require "active_support/core_ext/kernel/reporting"
+require "active_support/core_ext/load_error"
+require "active_support/core_ext/name_error"
+require "active_support/core_ext/string/starts_ends_with"
+require "active_support/dependencies/interlock"
+require "active_support/inflector"
module ActiveSupport #:nodoc:
module Dependencies #:nodoc:
@@ -447,6 +447,7 @@ module ActiveSupport #:nodoc:
mod = Module.new
into.const_set const_name, mod
autoloaded_constants << qualified_name unless autoload_once_paths.include?(base_path)
+ autoloaded_constants.uniq!
mod
end
@@ -615,7 +616,7 @@ module ActiveSupport #:nodoc:
return false if desc.is_a?(Module) && desc.anonymous?
name = to_constant_name desc
return false unless qualified_const_defined?(name)
- return autoloaded_constants.include?(name)
+ autoloaded_constants.include?(name)
end
# Will the provided constant descriptor be unloaded?
@@ -670,7 +671,7 @@ module ActiveSupport #:nodoc:
when Module
desc.name ||
raise(ArgumentError, "Anonymous modules have no name to be referenced by")
- else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}"
+ else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}"
end
end
diff --git a/activesupport/lib/active_support/dependencies/autoload.rb b/activesupport/lib/active_support/dependencies/autoload.rb
index 1c3775f5eb..1cee85d98f 100644
--- a/activesupport/lib/active_support/dependencies/autoload.rb
+++ b/activesupport/lib/active_support/dependencies/autoload.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../inflector/methods"
+require "active_support/inflector/methods"
module ActiveSupport
# Autoload and eager load conveniences for your library.
diff --git a/activesupport/lib/active_support/dependencies/interlock.rb b/activesupport/lib/active_support/dependencies/interlock.rb
index 4e9595ed42..948be75638 100644
--- a/activesupport/lib/active_support/dependencies/interlock.rb
+++ b/activesupport/lib/active_support/dependencies/interlock.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../concurrency/share_lock"
+require "active_support/concurrency/share_lock"
module ActiveSupport #:nodoc:
module Dependencies #:nodoc:
diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb
index e6550fdcd0..7271ab565b 100644
--- a/activesupport/lib/active_support/deprecation.rb
+++ b/activesupport/lib/active_support/deprecation.rb
@@ -14,13 +14,13 @@ module ActiveSupport
# a circular require warning for active_support/deprecation.rb.
#
# So, we define the constant first, and load dependencies later.
- require_relative "deprecation/instance_delegator"
- require_relative "deprecation/behaviors"
- require_relative "deprecation/reporting"
- require_relative "deprecation/constant_accessor"
- require_relative "deprecation/method_wrappers"
- require_relative "deprecation/proxy_wrappers"
- require_relative "core_ext/module/deprecation"
+ require "active_support/deprecation/instance_delegator"
+ require "active_support/deprecation/behaviors"
+ require "active_support/deprecation/reporting"
+ require "active_support/deprecation/constant_accessor"
+ require "active_support/deprecation/method_wrappers"
+ require "active_support/deprecation/proxy_wrappers"
+ require "active_support/core_ext/module/deprecation"
include Singleton
include InstanceDelegator
@@ -35,7 +35,7 @@ module ActiveSupport
# and the second is a library name.
#
# ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
- def initialize(deprecation_horizon = "6.0", gem_name = "Rails")
+ def initialize(deprecation_horizon = "6.1", gem_name = "Rails")
self.gem_name = gem_name
self.deprecation_horizon = deprecation_horizon
# By default, warnings are not silenced and debugging is off.
diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb
index 967320bcb9..66d6f3225a 100644
--- a/activesupport/lib/active_support/deprecation/behaviors.rb
+++ b/activesupport/lib/active_support/deprecation/behaviors.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../notifications"
+require "active_support/notifications"
module ActiveSupport
# Raised when <tt>ActiveSupport::Deprecation::Behavior#behavior</tt> is set with <tt>:raise</tt>.
@@ -27,7 +27,7 @@ module ActiveSupport
if defined?(Rails.logger) && Rails.logger
Rails.logger
else
- require_relative "../logger"
+ require "active_support/logger"
ActiveSupport::Logger.new($stderr)
end
logger.warn message
@@ -85,7 +85,7 @@ module ActiveSupport
# ActiveSupport::Deprecation.behavior = :stderr
# ActiveSupport::Deprecation.behavior = [:stderr, :log]
# ActiveSupport::Deprecation.behavior = MyCustomHandler
- # ActiveSupport::Deprecation.behavior = ->(message, callstack) {
+ # ActiveSupport::Deprecation.behavior = ->(message, callstack, deprecation_horizon, gem_name) {
# # custom stuff
# }
def behavior=(behavior)
diff --git a/activesupport/lib/active_support/deprecation/constant_accessor.rb b/activesupport/lib/active_support/deprecation/constant_accessor.rb
index 0dfd96d134..1ed0015812 100644
--- a/activesupport/lib/active_support/deprecation/constant_accessor.rb
+++ b/activesupport/lib/active_support/deprecation/constant_accessor.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require_relative "../inflector/methods"
-
module ActiveSupport
class Deprecation
# DeprecatedConstantAccessor transforms a constant into a deprecated one by
@@ -17,7 +15,7 @@ module ActiveSupport
#
# PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto)
#
- # (In a later update, the original implementation of `PLANETS` has been removed.)
+ # # (In a later update, the original implementation of `PLANETS` has been removed.)
#
# PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
# include ActiveSupport::Deprecation::DeprecatedConstantAccessor
@@ -29,6 +27,8 @@ module ActiveSupport
# ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
module DeprecatedConstantAccessor
def self.included(base)
+ require "active_support/inflector/methods"
+
extension = Module.new do
def const_missing(missing_const_name)
if class_variable_defined?(:@@_deprecated_constants)
diff --git a/activesupport/lib/active_support/deprecation/instance_delegator.rb b/activesupport/lib/active_support/deprecation/instance_delegator.rb
index 539357c83e..8beda373a2 100644
--- a/activesupport/lib/active_support/deprecation/instance_delegator.rb
+++ b/activesupport/lib/active_support/deprecation/instance_delegator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../core_ext/kernel/singleton_class"
-require_relative "../core_ext/module/delegation"
+require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/module/delegation"
module ActiveSupport
class Deprecation
diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb
index 8c942ed9a5..5be893d281 100644
--- a/activesupport/lib/active_support/deprecation/method_wrappers.rb
+++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb
@@ -1,16 +1,14 @@
# frozen_string_literal: true
-require_relative "../core_ext/module/aliasing"
-require_relative "../core_ext/array/extract_options"
+require "active_support/core_ext/module/aliasing"
+require "active_support/core_ext/array/extract_options"
module ActiveSupport
class Deprecation
module MethodWrapper
# Declare that a method has been deprecated.
#
- # module Fred
- # extend self
- #
+ # class Fred
# def aaa; end
# def bbb; end
# def ccc; end
@@ -22,15 +20,15 @@ module ActiveSupport
# ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead')
# # => Fred
#
- # Fred.aaa
+ # Fred.new.aaa
# # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.1. (called from irb_binding at (irb):10)
# # => nil
#
- # Fred.bbb
+ # Fred.new.bbb
# # DEPRECATION WARNING: bbb is deprecated and will be removed from Rails 5.1 (use zzz instead). (called from irb_binding at (irb):11)
# # => nil
#
- # Fred.ccc
+ # Fred.new.ccc
# # DEPRECATION WARNING: ccc is deprecated and will be removed from Rails 5.1 (use Bar#ccc instead). (called from irb_binding at (irb):12)
# # => nil
#
@@ -39,7 +37,7 @@ module ActiveSupport
# ActiveSupport::Deprecation.deprecate_methods(Fred, ddd: :zzz, deprecator: custom_deprecator)
# # => [:ddd]
#
- # Fred.ddd
+ # Fred.new.ddd
# DEPRECATION WARNING: ddd is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):15)
# # => nil
#
@@ -48,7 +46,7 @@ module ActiveSupport
# custom_deprecator.deprecate_methods(Fred, eee: :zzz)
# # => [:eee]
#
- # Fred.eee
+ # Fred.new.eee
# DEPRECATION WARNING: eee is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):18)
# # => nil
def deprecate_methods(target_module, *method_names)
@@ -62,6 +60,13 @@ module ActiveSupport
deprecator.deprecation_warning(method_name, options[method_name])
super(*args, &block)
end
+
+ case
+ when target_module.protected_method_defined?(method_name)
+ protected method_name
+ when target_module.private_method_defined?(method_name)
+ private method_name
+ end
end
end
diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
index 1920d75faf..896c0d2d8e 100644
--- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
+++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
-require_relative "../inflector/methods"
-require_relative "../core_ext/regexp"
+require "active_support/core_ext/regexp"
module ActiveSupport
class Deprecation
@@ -114,7 +113,7 @@ module ActiveSupport
#
# PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto)
#
- # (In a later update, the original implementation of `PLANETS` has been removed.)
+ # # (In a later update, the original implementation of `PLANETS` has been removed.)
#
# PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
# PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006')
@@ -125,6 +124,8 @@ module ActiveSupport
# ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
class DeprecatedConstantProxy < DeprecationProxy
def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance, message: "#{old_const} is deprecated! Use #{new_const} instead.")
+ require "active_support/inflector/methods"
+
@old_const = old_const
@new_const = new_const
@deprecator = deprecator
diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb
index 242e21b782..7075b5b869 100644
--- a/activesupport/lib/active_support/deprecation/reporting.rb
+++ b/activesupport/lib/active_support/deprecation/reporting.rb
@@ -61,7 +61,7 @@ module ActiveSupport
case message
when Symbol then "#{warning} (use #{message} instead)"
when String then "#{warning} (#{message})"
- else warning
+ else warning
end
end
@@ -104,7 +104,7 @@ module ActiveSupport
end
end
- RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__)
+ RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/"
def ignored_callstack(path)
path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG["rubylibdir"])
diff --git a/activesupport/lib/active_support/digest.rb b/activesupport/lib/active_support/digest.rb
new file mode 100644
index 0000000000..fba10fbdcf
--- /dev/null
+++ b/activesupport/lib/active_support/digest.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ class Digest #:nodoc:
+ class <<self
+ def hash_digest_class
+ @hash_digest_class ||= ::Digest::MD5
+ end
+
+ def hash_digest_class=(klass)
+ raise ArgumentError, "#{klass} is expected to implement hexdigest class method" unless klass.respond_to?(:hexdigest)
+ @hash_digest_class = klass
+ end
+
+ def hexdigest(arg)
+ hash_digest_class.hexdigest(arg)[0...32]
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb
index 34024fa8c6..88897f811e 100644
--- a/activesupport/lib/active_support/duration.rb
+++ b/activesupport/lib/active_support/duration.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
-require_relative "core_ext/array/conversions"
-require_relative "core_ext/module/delegation"
-require_relative "core_ext/object/acts_like"
-require_relative "core_ext/string/filters"
-require_relative "deprecation"
+require "active_support/core_ext/array/conversions"
+require "active_support/core_ext/module/delegation"
+require "active_support/core_ext/object/acts_like"
+require "active_support/core_ext/string/filters"
+require "active_support/deprecation"
module ActiveSupport
# Provides accurate date and time measurements using Date#advance and
@@ -194,7 +194,6 @@ module ActiveSupport
end
parts[:seconds] = remainder
- parts.reject! { |k, v| v.zero? }
new(value, parts)
end
@@ -211,6 +210,7 @@ module ActiveSupport
def initialize(value, parts) #:nodoc:
@value, @parts = value, parts.to_h
@parts.default = 0
+ @parts.reject! { |k, v| v.zero? }
end
def coerce(other) #:nodoc:
@@ -370,6 +370,8 @@ module ActiveSupport
alias :before :ago
def inspect #:nodoc:
+ return "0 seconds" if parts.empty?
+
parts.
reduce(::Hash.new(0)) { |h, (l, r)| h[l] += r; h }.
sort_by { |unit, _ | PARTS.index(unit) }.
@@ -381,6 +383,14 @@ module ActiveSupport
to_i
end
+ def init_with(coder) #:nodoc:
+ initialize(coder["value"], coder["parts"])
+ end
+
+ def encode_with(coder) #:nodoc:
+ coder.map = { "value" => @value, "parts" => @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)
diff --git a/activesupport/lib/active_support/duration/iso8601_parser.rb b/activesupport/lib/active_support/duration/iso8601_parser.rb
index a002424cd9..1847eeaa86 100644
--- a/activesupport/lib/active_support/duration/iso8601_parser.rb
+++ b/activesupport/lib/active_support/duration/iso8601_parser.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "strscan"
-require_relative "../core_ext/regexp"
+require "active_support/core_ext/regexp"
module ActiveSupport
class Duration
@@ -118,7 +118,7 @@ module ActiveSupport
raise_parsing_error "(only last part can be fractional)"
end
- return true
+ true
end
end
end
diff --git a/activesupport/lib/active_support/duration/iso8601_serializer.rb b/activesupport/lib/active_support/duration/iso8601_serializer.rb
index 985eac113f..84ae29c1ec 100644
--- a/activesupport/lib/active_support/duration/iso8601_serializer.rb
+++ b/activesupport/lib/active_support/duration/iso8601_serializer.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
-require_relative "../core_ext/object/blank"
-require_relative "../core_ext/hash/transform_values"
+require "active_support/core_ext/object/blank"
module ActiveSupport
class Duration
diff --git a/activesupport/lib/active_support/encrypted_configuration.rb b/activesupport/lib/active_support/encrypted_configuration.rb
index b403048627..3c6da10548 100644
--- a/activesupport/lib/active_support/encrypted_configuration.rb
+++ b/activesupport/lib/active_support/encrypted_configuration.rb
@@ -11,8 +11,9 @@ module ActiveSupport
delegate :[], :fetch, to: :config
delegate_missing_to :options
- def initialize(config_path:, key_path:, env_key:)
- super content_path: config_path, key_path: key_path, env_key: env_key
+ def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:)
+ super content_path: config_path, key_path: key_path,
+ env_key: env_key, raise_if_missing_key: raise_if_missing_key
end
# Allow a config to be started without a file present
@@ -22,6 +23,12 @@ module ActiveSupport
""
end
+ def write(contents)
+ deserialize(contents)
+
+ super
+ end
+
def config
@config ||= deserialize(read).deep_symbolize_keys
end
@@ -31,12 +38,8 @@ module ActiveSupport
@options ||= ActiveSupport::InheritableOptions.new(config)
end
- def serialize(config)
- config.present? ? YAML.dump(config) : ""
- end
-
def deserialize(config)
- config.present? ? YAML.load(config) : {}
+ config.present? ? YAML.load(config, content_path) : {}
end
end
end
diff --git a/activesupport/lib/active_support/encrypted_file.rb b/activesupport/lib/active_support/encrypted_file.rb
index 1c4c1cb457..c66f1b557e 100644
--- a/activesupport/lib/active_support/encrypted_file.rb
+++ b/activesupport/lib/active_support/encrypted_file.rb
@@ -2,8 +2,6 @@
require "pathname"
require "active_support/message_encryptor"
-require "active_support/core_ext/string/strip"
-require "active_support/core_ext/module/delegation"
module ActiveSupport
class EncryptedFile
@@ -28,11 +26,11 @@ module ActiveSupport
end
- attr_reader :content_path, :key_path, :env_key
+ attr_reader :content_path, :key_path, :env_key, :raise_if_missing_key
- def initialize(content_path:, key_path:, env_key:)
+ def initialize(content_path:, key_path:, env_key:, raise_if_missing_key:)
@content_path, @key_path = Pathname.new(content_path), Pathname.new(key_path)
- @env_key = env_key
+ @env_key, @raise_if_missing_key = env_key, raise_if_missing_key
end
def key
@@ -40,7 +38,7 @@ module ActiveSupport
end
def read
- if content_path.exist?
+ if !key.nil? && content_path.exist?
decrypt content_path.binread
else
raise MissingContentError, content_path
@@ -59,7 +57,7 @@ module ActiveSupport
private
def writing(contents)
- tmp_file = "#{content_path.basename}.#{Process.pid}"
+ tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}"
tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file)
tmp_path.binwrite contents
@@ -95,7 +93,7 @@ module ActiveSupport
end
def handle_missing_key
- raise MissingKeyError, key_path: key_path, env_key: env_key
+ raise MissingKeyError, key_path: key_path, env_key: env_key if raise_if_missing_key
end
end
end
diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb
index fd87d84795..f48c586cad 100644
--- a/activesupport/lib/active_support/execution_wrapper.rb
+++ b/activesupport/lib/active_support/execution_wrapper.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "callbacks"
+require "active_support/callbacks"
module ActiveSupport
class ExecutionWrapper
diff --git a/activesupport/lib/active_support/executor.rb b/activesupport/lib/active_support/executor.rb
index e6487ba69d..ce391b07ec 100644
--- a/activesupport/lib/active_support/executor.rb
+++ b/activesupport/lib/active_support/executor.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "execution_wrapper"
+require "active_support/execution_wrapper"
module ActiveSupport
class Executor < ExecutionWrapper
diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb
index bcf7b9de64..1a0bb10815 100644
--- a/activesupport/lib/active_support/file_update_checker.rb
+++ b/activesupport/lib/active_support/file_update_checker.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "core_ext/time/calculations"
+require "active_support/core_ext/time/calculations"
module ActiveSupport
# FileUpdateChecker specifies the API used by Rails to watch files
diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb
index 2a7ef2f820..c951ad16a3 100644
--- a/activesupport/lib/active_support/gem_version.rb
+++ b/activesupport/lib/active_support/gem_version.rb
@@ -7,8 +7,8 @@ module ActiveSupport
end
module VERSION
- MAJOR = 5
- MINOR = 2
+ MAJOR = 6
+ MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb
index 12291af443..e4afc8af93 100644
--- a/activesupport/lib/active_support/hash_with_indifferent_access.rb
+++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "core_ext/hash/keys"
-require_relative "core_ext/hash/reverse_merge"
+require "active_support/core_ext/hash/keys"
+require "active_support/core_ext/hash/reverse_merge"
module ActiveSupport
# Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered
@@ -177,20 +177,18 @@ module ActiveSupport
super(convert_key(key), *extras)
end
- if Hash.new.respond_to?(:dig)
- # Same as <tt>Hash#dig</tt> where the key passed as argument can be
- # either a string or a symbol:
- #
- # counters = ActiveSupport::HashWithIndifferentAccess.new
- # counters[:foo] = { bar: 1 }
- #
- # counters.dig('foo', 'bar') # => 1
- # counters.dig(:foo, :bar) # => 1
- # counters.dig(:zoo) # => nil
- def dig(*args)
- args[0] = convert_key(args[0]) if args.size > 0
- super(*args)
- end
+ # Same as <tt>Hash#dig</tt> where the key passed as argument can be
+ # either a string or a symbol:
+ #
+ # counters = ActiveSupport::HashWithIndifferentAccess.new
+ # counters[:foo] = { bar: 1 }
+ #
+ # counters.dig('foo', 'bar') # => 1
+ # counters.dig(:foo, :bar) # => 1
+ # counters.dig(:zoo) # => nil
+ def dig(*args)
+ args[0] = convert_key(args[0]) if args.size > 0
+ super(*args)
end
# Same as <tt>Hash#default</tt> where the key passed as argument can be
@@ -228,7 +226,7 @@ module ActiveSupport
# hash.fetch_values('a', 'c') # => KeyError: key not found: "c"
def fetch_values(*indices, &block)
indices.collect { |key| fetch(key, &block) }
- end if Hash.method_defined?(:fetch_values)
+ end
# Returns a shallow copy of the hash.
#
@@ -306,6 +304,29 @@ module ActiveSupport
dup.tap { |hash| hash.transform_values!(*args, &block) }
end
+ def transform_keys(*args, &block)
+ return to_enum(:transform_keys) unless block_given?
+ dup.tap { |hash| hash.transform_keys!(*args, &block) }
+ end
+
+ def transform_keys!
+ return enum_for(:transform_keys!) { size } unless block_given?
+ keys.each do |key|
+ self[yield(key)] = delete(key)
+ end
+ self
+ end
+
+ def slice(*keys)
+ keys.map! { |key| convert_key(key) }
+ self.class.new(super)
+ end
+
+ def slice!(*keys)
+ keys.map! { |key| convert_key(key) }
+ super
+ end
+
def compact
dup.tap(&:compact!)
end
diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb
index 80f1475630..39dab1cc71 100644
--- a/activesupport/lib/active_support/i18n.rb
+++ b/activesupport/lib/active_support/i18n.rb
@@ -1,15 +1,16 @@
# frozen_string_literal: true
-require_relative "core_ext/hash/deep_merge"
-require_relative "core_ext/hash/except"
-require_relative "core_ext/hash/slice"
+require "active_support/core_ext/hash/deep_merge"
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/slice"
begin
require "i18n"
rescue LoadError => e
$stderr.puts "The i18n gem is not available. Please add it to your Gemfile and run bundle install"
raise e
end
-require_relative "lazy_load_hooks"
+require "active_support/lazy_load_hooks"
ActiveSupport.run_load_hooks(:i18n)
I18n.load_path << File.expand_path("locale/en.yml", __dir__)
+I18n.load_path << File.expand_path("locale/en.rb", __dir__)
diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb
index 369aecff69..ce8bfbfd8c 100644
--- a/activesupport/lib/active_support/i18n_railtie.rb
+++ b/activesupport/lib/active_support/i18n_railtie.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
require "active_support"
-require_relative "file_update_checker"
-require_relative "core_ext/array/wrap"
+require "active_support/file_update_checker"
+require "active_support/core_ext/array/wrap"
# :enddoc:
diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb
index e8e1657111..baf1cb3038 100644
--- a/activesupport/lib/active_support/inflections.rb
+++ b/activesupport/lib/active_support/inflections.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "inflector/inflections"
+require "active_support/inflector/inflections"
#--
# Defines the standard inflection rules. These are the starting point for
diff --git a/activesupport/lib/active_support/inflector.rb b/activesupport/lib/active_support/inflector.rb
index a6adb15a18..d77f04c9c5 100644
--- a/activesupport/lib/active_support/inflector.rb
+++ b/activesupport/lib/active_support/inflector.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
# in case active_support/inflector is required without the rest of active_support
-require_relative "inflector/inflections"
-require_relative "inflector/transliterate"
-require_relative "inflector/methods"
+require "active_support/inflector/inflections"
+require "active_support/inflector/transliterate"
+require "active_support/inflector/methods"
-require_relative "inflections"
-require_relative "core_ext/string/inflections"
+require "active_support/inflections"
+require "active_support/core_ext/string/inflections"
diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb
index 36dfbc5f42..7e5dff1d6d 100644
--- a/activesupport/lib/active_support/inflector/inflections.rb
+++ b/activesupport/lib/active_support/inflector/inflections.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
require "concurrent/map"
-require_relative "../core_ext/array/prepend_and_append"
-require_relative "../core_ext/regexp"
-require_relative "../i18n"
+require "active_support/core_ext/array/prepend_and_append"
+require "active_support/core_ext/regexp"
+require "active_support/i18n"
+require "active_support/deprecation"
module ActiveSupport
module Inflector
@@ -67,16 +68,21 @@ module ActiveSupport
end
attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex
+ deprecate :acronym_regex
+
+ attr_reader :acronyms_camelize_regex, :acronyms_underscore_regex # :nodoc:
def initialize
- @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], Uncountables.new, [], {}, /(?=a)b/
+ @plurals, @singulars, @uncountables, @humans, @acronyms = [], [], Uncountables.new, [], {}
+ define_acronym_regex_patterns
end
# Private, for the test suite.
def initialize_dup(orig) # :nodoc:
- %w(plurals singulars uncountables humans acronyms acronym_regex).each do |scope|
+ %w(plurals singulars uncountables humans acronyms).each do |scope|
instance_variable_set("@#{scope}", orig.send(scope).dup)
end
+ define_acronym_regex_patterns
end
# Specifies a new acronym. An acronym must be specified as it will appear
@@ -130,7 +136,7 @@ module ActiveSupport
# camelize 'mcdonald' # => 'McDonald'
def acronym(word)
@acronyms[word.downcase] = word
- @acronym_regex = /#{@acronyms.values.join("|")}/
+ define_acronym_regex_patterns
end
# Specifies a new pluralization rule and its replacement. The rule can
@@ -221,10 +227,18 @@ module ActiveSupport
case scope
when :all
@plurals, @singulars, @uncountables, @humans = [], [], Uncountables.new, []
- else
+ else
instance_variable_set "@#{scope}", []
end
end
+
+ private
+
+ def define_acronym_regex_patterns
+ @acronym_regex = @acronyms.empty? ? /(?=a)b/ : /#{@acronyms.values.join("|")}/
+ @acronyms_camelize_regex = /^(?:#{@acronym_regex}(?=\b|[A-Z_])|\w)/
+ @acronyms_underscore_regex = /(?:(?<=([A-Za-z\d]))|\b)(#{@acronym_regex})(?=\b|[^a-z])/
+ end
end
# Yields a singleton instance of Inflector::Inflections so you can specify
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index 9398ed60a4..339b93b8da 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../inflections"
-require_relative "../core_ext/regexp"
+require "active_support/inflections"
+require "active_support/core_ext/regexp"
module ActiveSupport
# The Inflector transforms words from singular to plural, class names to table
@@ -71,7 +71,7 @@ module ActiveSupport
if uppercase_first_letter
string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize }
else
- string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { |match| match.downcase }
+ string = string.sub(inflections.acronyms_camelize_regex) { |match| match.downcase }
end
string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }
string.gsub!("/".freeze, "::".freeze)
@@ -92,7 +92,7 @@ module ActiveSupport
def underscore(camel_cased_word)
return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
word = camel_cased_word.to_s.gsub("::".freeze, "/".freeze)
- word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'.freeze }#{$2.downcase}" }
+ word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_'.freeze }#{$2.downcase}" }
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze)
word.tr!("-".freeze, "_".freeze)
@@ -138,7 +138,7 @@ module ActiveSupport
result.tr!("_".freeze, " ".freeze)
result.gsub!(/([a-z\d]*)/i) do |match|
- "#{inflections.acronyms[match] || match.downcase}"
+ "#{inflections.acronyms[match.downcase] || match.downcase}"
end
if capitalize
@@ -341,18 +341,7 @@ module ActiveSupport
# ordinal(-11) # => "th"
# ordinal(-1021) # => "st"
def ordinal(number)
- abs_number = number.to_i.abs
-
- if (11..13).include?(abs_number % 100)
- "th"
- else
- case abs_number % 10
- when 1; "st"
- when 2; "nd"
- when 3; "rd"
- else "th"
- end
- end
+ I18n.translate("number.nth.ordinals", number: number)
end
# Turns a number into an ordinal string used to denote the position in an
@@ -365,7 +354,7 @@ module ActiveSupport
# ordinalize(-11) # => "-11th"
# ordinalize(-1021) # => "-1021st"
def ordinalize(number)
- "#{number}#{ordinal(number)}"
+ I18n.translate("number.nth.ordinalized", number: number)
end
private
diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb
index aa7b21734e..6f2ca4999c 100644
--- a/activesupport/lib/active_support/inflector/transliterate.rb
+++ b/activesupport/lib/active_support/inflector/transliterate.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../core_ext/string/multibyte"
-require_relative "../i18n"
+require "active_support/core_ext/string/multibyte"
+require "active_support/i18n"
module ActiveSupport
module Inflector
@@ -71,17 +71,23 @@ module ActiveSupport
# a 'pretty' URL.
#
# parameterize("Donald E. Knuth") # => "donald-e-knuth"
- # parameterize("^trés|Jolie-- ") # => "tres-jolie"
+ # parameterize("^très|Jolie-- ") # => "tres-jolie"
#
- # To use a custom separator, override the `separator` argument.
+ # To use a custom separator, override the +separator+ argument.
#
# parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth"
- # parameterize("^trés|Jolie-- ", separator: '_') # => "tres_jolie"
+ # parameterize("^très|Jolie__ ", separator: '_') # => "tres_jolie"
#
- # To preserve the case of the characters in a string, use the `preserve_case` argument.
+ # To preserve the case of the characters in a string, use the +preserve_case+ argument.
#
# parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth"
- # parameterize("^trés|Jolie-- ", preserve_case: true) # => "tres-Jolie"
+ # parameterize("^très|Jolie-- ", preserve_case: true) # => "tres-Jolie"
+ #
+ # It preserves dashes and underscores unless they are used as separators:
+ #
+ # parameterize("^très|Jolie__ ") # => "tres-jolie__"
+ # parameterize("^très|Jolie-- ", separator: "_") # => "tres_jolie--"
+ # parameterize("^très_Jolie-- ", separator: ".") # => "tres_jolie--"
#
def parameterize(string, separator: "-", preserve_case: false)
# Replace accented chars with their ASCII equivalents.
diff --git a/activesupport/lib/active_support/json.rb b/activesupport/lib/active_support/json.rb
index b5672025fb..d7887175c0 100644
--- a/activesupport/lib/active_support/json.rb
+++ b/activesupport/lib/active_support/json.rb
@@ -1,4 +1,4 @@
# frozen_string_literal: true
-require_relative "json/decoding"
-require_relative "json/encoding"
+require "active_support/json/decoding"
+require "active_support/json/encoding"
diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb
index caa4082dde..8c0e016dc5 100644
--- a/activesupport/lib/active_support/json/decoding.rb
+++ b/activesupport/lib/active_support/json/decoding.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../core_ext/module/attribute_accessors"
-require_relative "../core_ext/module/delegation"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/module/delegation"
require "json"
module ActiveSupport
diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb
index 4016d13364..de1b8ac8cf 100644
--- a/activesupport/lib/active_support/json/encoding.rb
+++ b/activesupport/lib/active_support/json/encoding.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../core_ext/object/json"
-require_relative "../core_ext/module/delegation"
+require "active_support/core_ext/object/json"
+require "active_support/core_ext/module/delegation"
module ActiveSupport
class << self
@@ -54,9 +54,13 @@ module ActiveSupport
class EscapedString < String #:nodoc:
def to_json(*)
if Encoding.escape_html_entities_in_json
- super.gsub ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
+ s = super
+ s.gsub! ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
+ s
else
- super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
+ s = super
+ s.gsub! ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
+ s
end
end
diff --git a/activesupport/lib/active_support/locale/en.rb b/activesupport/lib/active_support/locale/en.rb
new file mode 100644
index 0000000000..26c2280c95
--- /dev/null
+++ b/activesupport/lib/active_support/locale/en.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+{
+ en: {
+ number: {
+ nth: {
+ ordinals: lambda do |_key, number:, **_options|
+ abs_number = number.to_i.abs
+
+ if (11..13).cover?(abs_number % 100)
+ "th"
+ else
+ case abs_number % 10
+ when 1 then "st"
+ when 2 then "nd"
+ when 3 then "rd"
+ else "th"
+ end
+ end
+ end,
+
+ ordinalized: lambda do |_key, number:, **_options|
+ "#{number}#{ActiveSupport::Inflector.ordinal(number)}"
+ end
+ }
+ }
+ }
+}
diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb
index 05a11221bf..0f7be06c8e 100644
--- a/activesupport/lib/active_support/log_subscriber.rb
+++ b/activesupport/lib/active_support/log_subscriber.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "core_ext/module/attribute_accessors"
-require_relative "core_ext/class/attribute"
-require_relative "subscriber"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/class/attribute"
+require "active_support/subscriber"
module ActiveSupport
# ActiveSupport::LogSubscriber is an object set to consume
diff --git a/activesupport/lib/active_support/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb
index 5b2abfc57c..3f19ef5009 100644
--- a/activesupport/lib/active_support/log_subscriber/test_helper.rb
+++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "../log_subscriber"
-require_relative "../logger"
-require_relative "../notifications"
+require "active_support/log_subscriber"
+require "active_support/logger"
+require "active_support/notifications"
module ActiveSupport
class LogSubscriber
diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb
index 3397ac4c9f..8152a182b4 100644
--- a/activesupport/lib/active_support/logger.rb
+++ b/activesupport/lib/active_support/logger.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "logger_silence"
-require_relative "logger_thread_safe_level"
+require "active_support/logger_silence"
+require "active_support/logger_thread_safe_level"
require "logger"
module ActiveSupport
diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb
index 693c7a1947..89f32b6782 100644
--- a/activesupport/lib/active_support/logger_silence.rb
+++ b/activesupport/lib/active_support/logger_silence.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "concern"
-require_relative "core_ext/module/attribute_accessors"
+require "active_support/concern"
+require "active_support/core_ext/module/attribute_accessors"
require "concurrent"
module LoggerSilence
diff --git a/activesupport/lib/active_support/logger_thread_safe_level.rb b/activesupport/lib/active_support/logger_thread_safe_level.rb
index 3c7f53d92c..ba32813d3d 100644
--- a/activesupport/lib/active_support/logger_thread_safe_level.rb
+++ b/activesupport/lib/active_support/logger_thread_safe_level.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "concern"
+require "active_support/concern"
module ActiveSupport
module LoggerThreadSafeLevel # :nodoc:
diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb
index 27620f56be..8b73270894 100644
--- a/activesupport/lib/active_support/message_encryptor.rb
+++ b/activesupport/lib/active_support/message_encryptor.rb
@@ -2,9 +2,10 @@
require "openssl"
require "base64"
-require_relative "core_ext/array/extract_options"
-require_relative "message_verifier"
-require_relative "messages/metadata"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/message_verifier"
+require "active_support/messages/metadata"
module ActiveSupport
# MessageEncryptor is a simple way to encrypt values which get stored
@@ -54,10 +55,36 @@ module ActiveSupport
#
# Then the messages can be verified and returned upto the expire time.
# Thereafter, verifying returns +nil+.
+ #
+ # === Rotating keys
+ #
+ # MessageEncryptor also supports rotating out old configurations by falling
+ # back to a stack of encryptors. Call +rotate+ to build and add an encryptor
+ # so +decrypt_and_verify+ will also try the fallback.
+ #
+ # By default any rotated encryptors use the values of the primary
+ # encryptor unless specified otherwise.
+ #
+ # You'd give your encryptor the new defaults:
+ #
+ # crypt = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
+ #
+ # Then gradually rotate the old values out by adding them as fallbacks. Any message
+ # generated with the old values will then work until the rotation is removed.
+ #
+ # crypt.rotate old_secret # Fallback to an old secret instead of @secret.
+ # crypt.rotate cipher: "aes-256-cbc" # Fallback to an old cipher instead of aes-256-gcm.
+ #
+ # Though if both the secret and the cipher was changed at the same time,
+ # the above should be combined into:
+ #
+ # crypt.rotate old_secret, cipher: "aes-256-cbc"
class MessageEncryptor
- class << self
- attr_accessor :use_authenticated_message_encryption #:nodoc:
+ prepend Messages::Rotator::Encryptor
+
+ cattr_accessor :use_authenticated_message_encryption, instance_accessor: false, default: false
+ class << self
def default_cipher #:nodoc:
if use_authenticated_message_encryption
"aes-256-gcm"
@@ -126,7 +153,7 @@ module ActiveSupport
# Decrypt and verify a message. We need to verify the message in order to
# avoid padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/.
- def decrypt_and_verify(data, purpose: nil)
+ def decrypt_and_verify(data, purpose: nil, **)
_decrypt(verifier.verify(data), purpose)
end
diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb
index 7110d6d2c9..83c39c0a86 100644
--- a/activesupport/lib/active_support/message_verifier.rb
+++ b/activesupport/lib/active_support/message_verifier.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
require "base64"
-require_relative "core_ext/object/blank"
-require_relative "security_utils"
-require_relative "messages/metadata"
+require "active_support/core_ext/object/blank"
+require "active_support/security_utils"
+require "active_support/messages/metadata"
+require "active_support/messages/rotator"
module ActiveSupport
# +MessageVerifier+ makes it easy to generate and verify messages which are
@@ -30,7 +31,7 @@ module ActiveSupport
#
# +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:
+ # +:digest+ key as an option while initializing the verifier:
#
# @verifier = ActiveSupport::MessageVerifier.new('s3Krit', digest: 'SHA256')
#
@@ -73,7 +74,33 @@ module ActiveSupport
# Then the messages can be verified and returned upto the expire time.
# Thereafter, the +verified+ method returns +nil+ while +verify+ raises
# <tt>ActiveSupport::MessageVerifier::InvalidSignature</tt>.
+ #
+ # === Rotating keys
+ #
+ # MessageVerifier also supports rotating out old configurations by falling
+ # back to a stack of verifiers. Call +rotate+ to build and add a verifier to
+ # so either +verified+ or +verify+ will also try verifying with the fallback.
+ #
+ # By default any rotated verifiers use the values of the primary
+ # verifier unless specified otherwise.
+ #
+ # You'd give your verifier the new defaults:
+ #
+ # verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512", serializer: JSON)
+ #
+ # Then gradually rotate the old values out by adding them as fallbacks. Any message
+ # generated with the old values will then work until the rotation is removed.
+ #
+ # verifier.rotate old_secret # Fallback to an old secret instead of @secret.
+ # verifier.rotate digest: "SHA256" # Fallback to an old digest instead of SHA512.
+ # verifier.rotate serializer: Marshal # Fallback to an old serializer instead of JSON.
+ #
+ # Though the above would most likely be combined into one rotation:
+ #
+ # verifier.rotate old_secret, digest: "SHA256", serializer: Marshal
class MessageVerifier
+ prepend Messages::Rotator::Verifier
+
class InvalidSignature < StandardError; end
def initialize(secret, options = {})
@@ -120,7 +147,7 @@ module ActiveSupport
#
# incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff"
# verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format
- def verified(signed_message, purpose: nil)
+ def verified(signed_message, purpose: nil, **)
if valid_message?(signed_message)
begin
data = signed_message.split("--".freeze)[0]
@@ -145,8 +172,8 @@ module ActiveSupport
#
# other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
# other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature
- def verify(signed_message, purpose: nil)
- verified(signed_message, purpose: purpose) || raise(InvalidSignature)
+ def verify(*args)
+ verified(*args) || raise(InvalidSignature)
end
# Generates a signed message for the provided value.
diff --git a/activesupport/lib/active_support/messages/rotation_configuration.rb b/activesupport/lib/active_support/messages/rotation_configuration.rb
new file mode 100644
index 0000000000..bd50d6d348
--- /dev/null
+++ b/activesupport/lib/active_support/messages/rotation_configuration.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module Messages
+ class RotationConfiguration # :nodoc:
+ attr_reader :signed, :encrypted
+
+ def initialize
+ @signed, @encrypted = [], []
+ end
+
+ def rotate(kind, *args)
+ case kind
+ when :signed
+ @signed << args
+ when :encrypted
+ @encrypted << args
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/messages/rotator.rb b/activesupport/lib/active_support/messages/rotator.rb
new file mode 100644
index 0000000000..823a399d67
--- /dev/null
+++ b/activesupport/lib/active_support/messages/rotator.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module Messages
+ module Rotator # :nodoc:
+ def initialize(*, **options)
+ super
+
+ @options = options
+ @rotations = []
+ end
+
+ def rotate(*secrets, **options)
+ @rotations << build_rotation(*secrets, @options.merge(options))
+ end
+
+ module Encryptor
+ include Rotator
+
+ def decrypt_and_verify(*args, on_rotation: nil, **options)
+ super
+ rescue MessageEncryptor::InvalidMessage, MessageVerifier::InvalidSignature
+ run_rotations(on_rotation) { |encryptor| encryptor.decrypt_and_verify(*args, options) } || raise
+ end
+
+ private
+ def build_rotation(secret = @secret, sign_secret = @sign_secret, options)
+ self.class.new(secret, sign_secret, options)
+ end
+ end
+
+ module Verifier
+ include Rotator
+
+ def verified(*args, on_rotation: nil, **options)
+ super || run_rotations(on_rotation) { |verifier| verifier.verified(*args, options) }
+ end
+
+ private
+ def build_rotation(secret = @secret, options)
+ self.class.new(secret, options)
+ end
+ end
+
+ private
+ def run_rotations(on_rotation)
+ @rotations.find do |rotation|
+ if message = yield(rotation) rescue next
+ on_rotation.call if on_rotation
+ return message
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb
index d676827e8e..8152b8fd22 100644
--- a/activesupport/lib/active_support/multibyte/chars.rb
+++ b/activesupport/lib/active_support/multibyte/chars.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
-require_relative "../json"
-require_relative "../core_ext/string/access"
-require_relative "../core_ext/string/behavior"
-require_relative "../core_ext/module/delegation"
-require_relative "../core_ext/regexp"
+require "active_support/json"
+require "active_support/core_ext/string/access"
+require "active_support/core_ext/string/behavior"
+require "active_support/core_ext/module/delegation"
+require "active_support/core_ext/regexp"
module ActiveSupport #:nodoc:
module Multibyte #:nodoc:
diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb
index a64223c0e0..4f0e1165ef 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -11,7 +11,7 @@ module ActiveSupport
NORMALIZATION_FORMS = [:c, :kc, :d, :kd]
# The Unicode version that is supported by the implementation
- UNICODE_VERSION = "9.0.0"
+ UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"]
# The default normalization used for operations that require
# normalization. It can be set to any of the normalizations
@@ -21,96 +21,13 @@ module ActiveSupport
attr_accessor :default_normalization_form
@default_normalization_form = :kc
- # Hangul character boundaries and properties
- HANGUL_SBASE = 0xAC00
- HANGUL_LBASE = 0x1100
- HANGUL_VBASE = 0x1161
- HANGUL_TBASE = 0x11A7
- HANGUL_LCOUNT = 19
- HANGUL_VCOUNT = 21
- HANGUL_TCOUNT = 28
- HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT
- HANGUL_SCOUNT = 11172
- HANGUL_SLAST = HANGUL_SBASE + HANGUL_SCOUNT
-
- # Detect whether the codepoint is in a certain character class. Returns
- # +true+ when it's in the specified character class and +false+ otherwise.
- # Valid character classes are: <tt>:cr</tt>, <tt>:lf</tt>, <tt>:l</tt>,
- # <tt>:v</tt>, <tt>:lv</tt>, <tt>:lvt</tt> and <tt>:t</tt>.
- #
- # Primarily used by the grapheme cluster support.
- def in_char_class?(codepoint, classes)
- classes.detect { |c| database.boundary[c] === codepoint } ? true : false
- end
-
# Unpack the string at grapheme boundaries. Returns a list of character
# lists.
#
# Unicode.unpack_graphemes('क्षि') # => [[2325, 2381], [2359], [2367]]
# Unicode.unpack_graphemes('Café') # => [[67], [97], [102], [233]]
def unpack_graphemes(string)
- codepoints = string.codepoints.to_a
- unpacked = []
- pos = 0
- marker = 0
- eoc = codepoints.length
- while (pos < eoc)
- pos += 1
- previous = codepoints[pos - 1]
- current = codepoints[pos]
-
- # See http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules
- should_break =
- if pos == eoc
- true
- # GB3. CR X LF
- elsif previous == database.boundary[:cr] && current == database.boundary[:lf]
- false
- # GB4. (Control|CR|LF) ÷
- elsif previous && in_char_class?(previous, [:control, :cr, :lf])
- true
- # GB5. ÷ (Control|CR|LF)
- elsif in_char_class?(current, [:control, :cr, :lf])
- true
- # GB6. L X (L|V|LV|LVT)
- elsif database.boundary[:l] === previous && in_char_class?(current, [:l, :v, :lv, :lvt])
- false
- # GB7. (LV|V) X (V|T)
- elsif in_char_class?(previous, [:lv, :v]) && in_char_class?(current, [:v, :t])
- false
- # GB8. (LVT|T) X (T)
- elsif in_char_class?(previous, [:lvt, :t]) && database.boundary[:t] === current
- false
- # GB9. X (Extend | ZWJ)
- elsif in_char_class?(current, [:extend, :zwj])
- false
- # GB9a. X SpacingMark
- elsif database.boundary[:spacingmark] === current
- false
- # GB9b. Prepend X
- elsif database.boundary[:prepend] === previous
- false
- # GB10. (E_Base | EBG) Extend* X E_Modifier
- elsif (marker...pos).any? { |i| in_char_class?(codepoints[i], [:e_base, :e_base_gaz]) && codepoints[i + 1...pos].all? { |c| database.boundary[:extend] === c } } && database.boundary[:e_modifier] === current
- false
- # GB11. ZWJ X (Glue_After_Zwj | EBG)
- elsif database.boundary[:zwj] === previous && in_char_class?(current, [:glue_after_zwj, :e_base_gaz])
- false
- # GB12. ^ (RI RI)* RI X RI
- # GB13. [^RI] (RI RI)* RI X RI
- elsif codepoints[marker..pos].all? { |c| database.boundary[:regional_indicator] === c } && codepoints[marker..pos].count { |c| database.boundary[:regional_indicator] === c }.even?
- false
- # GB999. Any ÷ Any
- else
- true
- end
-
- if should_break
- unpacked << codepoints[marker..pos - 1]
- marker = pos
- end
- end
- unpacked
+ string.scan(/\X/).map(&:codepoints)
end
# Reverse operation of unpack_graphemes.
@@ -120,100 +37,18 @@ module ActiveSupport
unpacked.flatten.pack("U*")
end
- # Re-order codepoints so the string becomes canonical.
- def reorder_characters(codepoints)
- length = codepoints.length - 1
- pos = 0
- while pos < length do
- cp1, cp2 = database.codepoints[codepoints[pos]], database.codepoints[codepoints[pos + 1]]
- if (cp1.combining_class > cp2.combining_class) && (cp2.combining_class > 0)
- codepoints[pos..pos + 1] = cp2.code, cp1.code
- pos += (pos > 0 ? -1 : 1)
- else
- pos += 1
- end
- end
- codepoints
- end
-
# Decompose composed characters to the decomposed form.
def decompose(type, codepoints)
- codepoints.inject([]) do |decomposed, cp|
- # if it's a hangul syllable starter character
- if HANGUL_SBASE <= cp && cp < HANGUL_SLAST
- sindex = cp - HANGUL_SBASE
- ncp = [] # new codepoints
- ncp << HANGUL_LBASE + sindex / HANGUL_NCOUNT
- ncp << HANGUL_VBASE + (sindex % HANGUL_NCOUNT) / HANGUL_TCOUNT
- tindex = sindex % HANGUL_TCOUNT
- ncp << (HANGUL_TBASE + tindex) unless tindex == 0
- decomposed.concat ncp
- # if the codepoint is decomposable in with the current decomposition type
- elsif (ncp = database.codepoints[cp].decomp_mapping) && (!database.codepoints[cp].decomp_type || type == :compatibility)
- decomposed.concat decompose(type, ncp.dup)
- else
- decomposed << cp
- end
+ if type == :compatibility
+ codepoints.pack("U*").unicode_normalize(:nfkd).codepoints
+ else
+ codepoints.pack("U*").unicode_normalize(:nfd).codepoints
end
end
# Compose decomposed characters to the composed form.
def compose(codepoints)
- pos = 0
- eoa = codepoints.length - 1
- starter_pos = 0
- starter_char = codepoints[0]
- previous_combining_class = -1
- while pos < eoa
- pos += 1
- lindex = starter_char - HANGUL_LBASE
- # -- Hangul
- if 0 <= lindex && lindex < HANGUL_LCOUNT
- vindex = codepoints[starter_pos + 1] - HANGUL_VBASE rescue vindex = -1
- if 0 <= vindex && vindex < HANGUL_VCOUNT
- tindex = codepoints[starter_pos + 2] - HANGUL_TBASE rescue tindex = -1
- if 0 <= tindex && tindex < HANGUL_TCOUNT
- j = starter_pos + 2
- eoa -= 2
- else
- tindex = 0
- j = starter_pos + 1
- eoa -= 1
- end
- codepoints[starter_pos..j] = (lindex * HANGUL_VCOUNT + vindex) * HANGUL_TCOUNT + tindex + HANGUL_SBASE
- end
- starter_pos += 1
- starter_char = codepoints[starter_pos]
- # -- Other characters
- else
- current_char = codepoints[pos]
- current = database.codepoints[current_char]
- if current.combining_class > previous_combining_class
- if ref = database.composition_map[starter_char]
- composition = ref[current_char]
- else
- composition = nil
- end
- unless composition.nil?
- codepoints[starter_pos] = composition
- starter_char = composition
- codepoints.delete_at pos
- eoa -= 1
- pos -= 1
- previous_combining_class = -1
- else
- previous_combining_class = current.combining_class
- end
- else
- previous_combining_class = current.combining_class
- end
- if current.combining_class == 0
- starter_pos = pos
- starter_char = codepoints[pos]
- end
- end
- end
- codepoints
+ codepoints.pack("U*").unicode_normalize(:nfc).codepoints
end
# Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars.
@@ -266,129 +101,37 @@ module ActiveSupport
def normalize(string, form = nil)
form ||= @default_normalization_form
# See http://www.unicode.org/reports/tr15, Table 1
- codepoints = string.codepoints.to_a
case form
when :d
- reorder_characters(decompose(:canonical, codepoints))
+ string.unicode_normalize(:nfd)
when :c
- compose(reorder_characters(decompose(:canonical, codepoints)))
+ string.unicode_normalize(:nfc)
when :kd
- reorder_characters(decompose(:compatibility, codepoints))
+ string.unicode_normalize(:nfkd)
when :kc
- compose(reorder_characters(decompose(:compatibility, codepoints)))
- else
+ string.unicode_normalize(:nfkc)
+ else
raise ArgumentError, "#{form} is not a valid normalization variant", caller
- end.pack("U*".freeze)
+ end
end
def downcase(string)
- apply_mapping string, :lowercase_mapping
+ string.downcase
end
def upcase(string)
- apply_mapping string, :uppercase_mapping
+ string.upcase
end
def swapcase(string)
- apply_mapping string, :swapcase_mapping
- end
-
- # Holds data about a codepoint in the Unicode database.
- class Codepoint
- attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping
-
- # Initializing Codepoint object with default values
- def initialize
- @combining_class = 0
- @uppercase_mapping = 0
- @lowercase_mapping = 0
- end
-
- def swapcase_mapping
- uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping
- end
- end
-
- # Holds static data from the Unicode database.
- class UnicodeDatabase
- ATTRIBUTES = :codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252
-
- attr_writer(*ATTRIBUTES)
-
- def initialize
- @codepoints = Hash.new(Codepoint.new)
- @composition_exclusion = []
- @composition_map = {}
- @boundary = {}
- @cp1252 = {}
- end
-
- # Lazy load the Unicode database so it's only loaded when it's actually used
- ATTRIBUTES.each do |attr_name|
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- def #{attr_name} # def codepoints
- load # load
- @#{attr_name} # @codepoints
- end # end
- EOS
- end
-
- # Loads the Unicode database and returns all the internal objects of
- # UnicodeDatabase.
- def load
- begin
- @codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, "rb") { |f| Marshal.load f.read }
- rescue => e
- raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
- end
-
- # Redefine the === method so we can write shorter rules for grapheme cluster breaks
- @boundary.each_key do |k|
- @boundary[k].instance_eval do
- def ===(other)
- detect { |i| i === other } ? true : false
- end
- end if @boundary[k].kind_of?(Array)
- end
-
- # define attr_reader methods for the instance variables
- class << self
- attr_reader(*ATTRIBUTES)
- end
- end
-
- # Returns the directory in which the data files are stored.
- def self.dirname
- File.expand_path("../values", __dir__)
- end
-
- # Returns the filename for the data file for this version.
- def self.filename
- File.expand_path File.join(dirname, "unicode_tables.dat")
- end
+ string.swapcase
end
private
- def apply_mapping(string, mapping)
- database.codepoints
- string.each_codepoint.map do |codepoint|
- cp = database.codepoints[codepoint]
- if cp && (ncp = cp.send(mapping)) && ncp > 0
- ncp
- else
- codepoint
- end
- end.pack("U*")
- end
-
def recode_windows1252_chars(string)
string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace)
end
-
- def database
- @database ||= UnicodeDatabase.new
- end
end
end
end
diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb
index 96a3463905..6207de8094 100644
--- a/activesupport/lib/active_support/notifications.rb
+++ b/activesupport/lib/active_support/notifications.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "notifications/instrumenter"
-require_relative "notifications/fanout"
-require_relative "per_thread_registry"
+require "active_support/notifications/instrumenter"
+require "active_support/notifications/fanout"
+require "active_support/per_thread_registry"
module ActiveSupport
# = Notifications
diff --git a/activesupport/lib/active_support/number_helper/number_converter.rb b/activesupport/lib/active_support/number_helper/number_converter.rb
index 5ea9c8f113..06ba797a13 100644
--- a/activesupport/lib/active_support/number_helper/number_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_converter.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
-require_relative "../core_ext/big_decimal/conversions"
-require_relative "../core_ext/object/blank"
-require_relative "../core_ext/hash/keys"
-require_relative "../i18n"
-require_relative "../core_ext/class/attribute"
+require "active_support/core_ext/big_decimal/conversions"
+require "active_support/core_ext/object/blank"
+require "active_support/core_ext/hash/keys"
+require "active_support/i18n"
+require "active_support/core_ext/class/attribute"
module ActiveSupport
module NumberHelper
diff --git a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
index 1943b9e295..a25e22cbd3 100644
--- a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require_relative "../core_ext/numeric/inquiry"
-
module ActiveSupport
module NumberHelper
class NumberToCurrencyConverter < NumberConverter # :nodoc:
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 3b62fe6819..eb528a0583 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
@@ -37,26 +37,6 @@ module ActiveSupport
private
- def digits_and_rounded_number(precision)
- if zero?
- [1, 0]
- else
- digits = digit_count(number)
- multiplier = 10**(digits - precision)
- rounded_number = calculate_rounded_number(multiplier)
- digits = digit_count(rounded_number) # After rounding, the number of digits may have changed
- [digits, rounded_number]
- end
- end
-
- def calculate_rounded_number(multiplier)
- (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier
- end
-
- def digit_count(number)
- number.zero? ? 1 : (Math.log10(absolute_number(number)) + 1).floor
- end
-
def strip_insignificant_zeros
options[:strip_insignificant_zeros]
end
diff --git a/activesupport/lib/active_support/number_helper/rounding_helper.rb b/activesupport/lib/active_support/number_helper/rounding_helper.rb
index a5b28296a2..2ad8d49c4e 100644
--- a/activesupport/lib/active_support/number_helper/rounding_helper.rb
+++ b/activesupport/lib/active_support/number_helper/rounding_helper.rb
@@ -36,7 +36,7 @@ module ActiveSupport
return 0 if number.zero?
digits = digit_count(number)
multiplier = 10**(digits - precision)
- (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier
+ (number / BigDecimal(multiplier.to_f.to_s)).round * multiplier
end
def convert_to_decimal(number)
diff --git a/activesupport/lib/active_support/option_merger.rb b/activesupport/lib/active_support/option_merger.rb
index 42cbbe7c42..ab9ca727f6 100644
--- a/activesupport/lib/active_support/option_merger.rb
+++ b/activesupport/lib/active_support/option_merger.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "core_ext/hash/deep_merge"
+require "active_support/core_ext/hash/deep_merge"
module ActiveSupport
class OptionMerger #:nodoc:
diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb
index fa7825b3ba..c4e419f546 100644
--- a/activesupport/lib/active_support/ordered_options.rb
+++ b/activesupport/lib/active_support/ordered_options.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "core_ext/object/blank"
+require "active_support/core_ext/object/blank"
module ActiveSupport
# Usually key value pairs are handled something like this:
@@ -24,7 +24,7 @@ module ActiveSupport
# To raise an exception when the value is blank, append a
# bang to the key name, like:
#
- # h.dog! # => raises KeyError: key not found: :dog
+ # h.dog! # => raises KeyError: :dog is blank
#
class OrderedOptions < Hash
alias_method :_get, :[] # preserve the original #[] method
@@ -46,7 +46,7 @@ module ActiveSupport
bangs = name_string.chomp!("!")
if bangs
- fetch(name_string.to_sym).presence || raise(KeyError.new("#{name_string} is blank."))
+ self[name_string].presence || raise(KeyError.new(":#{name_string} is blank"))
else
self[name_string]
end
diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb
index dd0cc6a604..eb92fb4371 100644
--- a/activesupport/lib/active_support/per_thread_registry.rb
+++ b/activesupport/lib/active_support/per_thread_registry.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "core_ext/module/delegation"
+require "active_support/core_ext/module/delegation"
module ActiveSupport
# NOTE: This approach has been deprecated for end-user code in favor of {thread_mattr_accessor}[rdoc-ref:Module#thread_mattr_accessor] and friends.
diff --git a/activesupport/lib/active_support/rails.rb b/activesupport/lib/active_support/rails.rb
index fe82d1cc14..8b727a69ec 100644
--- a/activesupport/lib/active_support/rails.rb
+++ b/activesupport/lib/active_support/rails.rb
@@ -11,25 +11,19 @@
# Rails and can change anytime.
# Defines Object#blank? and Object#present?.
-require_relative "core_ext/object/blank"
+require "active_support/core_ext/object/blank"
# Rails own autoload, eager_load, etc.
-require_relative "dependencies/autoload"
+require "active_support/dependencies/autoload"
# Support for ClassMethods and the included macro.
-require_relative "concern"
+require "active_support/concern"
# Defines Class#class_attribute.
-require_relative "core_ext/class/attribute"
+require "active_support/core_ext/class/attribute"
# Defines Module#delegate.
-require_relative "core_ext/module/delegation"
+require "active_support/core_ext/module/delegation"
# Defines ActiveSupport::Deprecation.
-require_relative "deprecation"
-
-# Defines Regexp#match?.
-#
-# This should be removed when Rails needs Ruby 2.4 or later, and the require
-# added where other Regexp extensions are being used (easy to grep).
-require_relative "core_ext/regexp"
+require "active_support/deprecation"
diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb
index fe132ca7c6..605b50d346 100644
--- a/activesupport/lib/active_support/railtie.rb
+++ b/activesupport/lib/active_support/railtie.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "active_support"
-require_relative "i18n_railtie"
+require "active_support/i18n_railtie"
module ActiveSupport
class Railtie < Rails::Railtie # :nodoc:
@@ -10,9 +10,11 @@ module ActiveSupport
config.eager_load_namespaces << ActiveSupport
initializer "active_support.set_authenticated_message_encryption" do |app|
- if app.config.active_support.respond_to?(:use_authenticated_message_encryption)
- ActiveSupport::MessageEncryptor.use_authenticated_message_encryption =
- app.config.active_support.use_authenticated_message_encryption
+ config.after_initialize do
+ unless app.config.active_support.use_authenticated_message_encryption.nil?
+ ActiveSupport::MessageEncryptor.use_authenticated_message_encryption =
+ app.config.active_support.use_authenticated_message_encryption
+ end
end
end
@@ -36,14 +38,14 @@ module ActiveSupport
rescue TZInfo::DataSourceNotFound => e
raise e.exception "tzinfo-data is not present. Please add gem 'tzinfo-data' to your Gemfile and run bundle install"
end
- require_relative "core_ext/time/zones"
+ require "active_support/core_ext/time/zones"
Time.zone_default = Time.find_zone!(app.config.time_zone)
end
# Sets the default week start
# If assigned value is not a valid day symbol (e.g. :sunday, :monday, ...), an exception will be raised.
initializer "active_support.initialize_beginning_of_week" do |app|
- require_relative "core_ext/date/calculations"
+ require "active_support/core_ext/date/calculations"
beginning_of_week_default = Date.find_beginning_of_week!(app.config.beginning_of_week)
Date.beginning_of_week_default = beginning_of_week_default
@@ -66,5 +68,13 @@ module ActiveSupport
ActiveSupport.send(k, v) if ActiveSupport.respond_to? k
end
end
+
+ initializer "active_support.set_hash_digest_class" do |app|
+ config.after_initialize do
+ if app.config.active_support.use_sha1_digests
+ ActiveSupport::Digest.hash_digest_class = ::Digest::SHA1
+ end
+ end
+ end
end
end
diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb
index 44062e3491..b26d9c3665 100644
--- a/activesupport/lib/active_support/reloader.rb
+++ b/activesupport/lib/active_support/reloader.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "execution_wrapper"
+require "active_support/execution_wrapper"
module ActiveSupport
#--
@@ -28,14 +28,17 @@ module ActiveSupport
define_callbacks :class_unload
+ # Registers a callback that will run once at application startup and every time the code is reloaded.
def self.to_prepare(*args, &block)
set_callback(:prepare, *args, &block)
end
+ # Registers a callback that will run immediately before the classes are unloaded.
def self.before_class_unload(*args, &block)
set_callback(:class_unload, *args, &block)
end
+ # Registers a callback that will run immediately after the classes are unloaded.
def self.after_class_unload(*args, &block)
set_callback(:class_unload, :after, *args, &block)
end
diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb
index 2f6aeef48a..e0fa29cacb 100644
--- a/activesupport/lib/active_support/rescuable.rb
+++ b/activesupport/lib/active_support/rescuable.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "concern"
-require_relative "core_ext/class/attribute"
-require_relative "core_ext/string/inflections"
+require "active_support/concern"
+require "active_support/core_ext/class/attribute"
+require "active_support/core_ext/string/inflections"
module ActiveSupport
# Rescuable module adds support for easier exception handling.
diff --git a/activesupport/lib/active_support/security_utils.rb b/activesupport/lib/active_support/security_utils.rb
index 51870559ec..20b6b9cd3f 100644
--- a/activesupport/lib/active_support/security_utils.rb
+++ b/activesupport/lib/active_support/security_utils.rb
@@ -1,17 +1,15 @@
# frozen_string_literal: true
-require "digest"
+require "digest/sha2"
module ActiveSupport
module SecurityUtils
- # Constant time string comparison.
+ # Constant time string comparison, for fixed length strings.
#
# The values compared should be of fixed length, such as strings
- # that have already been processed by HMAC. This should not be used
- # on variable length plaintext strings because it could leak length info
- # via timing attacks.
- def secure_compare(a, b)
- return false unless a.bytesize == b.bytesize
+ # that have already been processed by HMAC. Raises in case of length mismatch.
+ def fixed_length_secure_compare(a, b)
+ raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
l = a.unpack "C#{a.bytesize}"
@@ -19,11 +17,15 @@ module ActiveSupport
b.each_byte { |byte| res |= byte ^ l.shift }
res == 0
end
- module_function :secure_compare
+ module_function :fixed_length_secure_compare
- def variable_size_secure_compare(a, b) # :nodoc:
- secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b))
+ # Constant time string comparison, for variable length strings.
+ #
+ # The values are first processed by SHA256, so that we don't leak length info
+ # via timing attacks.
+ def secure_compare(a, b)
+ fixed_length_secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b)) && a == b
end
- module_function :variable_size_secure_compare
+ module_function :secure_compare
end
end
diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb
index 7913bb815e..8ad39f7a05 100644
--- a/activesupport/lib/active_support/subscriber.rb
+++ b/activesupport/lib/active_support/subscriber.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "per_thread_registry"
-require_relative "notifications"
+require "active_support/per_thread_registry"
+require "active_support/notifications"
module ActiveSupport
# ActiveSupport::Subscriber is an object set to consume
@@ -54,25 +54,20 @@ module ActiveSupport
@@subscribers ||= []
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :subscriber, :notifier, :namespace
-
private
+ attr_reader :subscriber, :notifier, :namespace
- def add_event_subscriber(event) # :doc:
- return if %w{ start finish }.include?(event.to_s)
+ def add_event_subscriber(event) # :doc:
+ return if %w{ start finish }.include?(event.to_s)
- pattern = "#{event}.#{namespace}"
+ pattern = "#{event}.#{namespace}"
- # Don't add multiple subscribers (eg. if methods are redefined).
- return if subscriber.patterns.include?(pattern)
+ # Don't add multiple subscribers (eg. if methods are redefined).
+ return if subscriber.patterns.include?(pattern)
- subscriber.patterns << pattern
- notifier.subscribe(pattern, subscriber)
- end
+ subscriber.patterns << pattern
+ notifier.subscribe(pattern, subscriber)
+ end
end
attr_reader :patterns # :nodoc:
diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb
index fe13eaed4e..b069ac94d4 100644
--- a/activesupport/lib/active_support/tagged_logging.rb
+++ b/activesupport/lib/active_support/tagged_logging.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-require_relative "core_ext/module/delegation"
-require_relative "core_ext/object/blank"
+require "active_support/core_ext/module/delegation"
+require "active_support/core_ext/object/blank"
require "logger"
-require_relative "logger"
+require "active_support/logger"
module ActiveSupport
# Wraps any standard Logger object to provide tagging capabilities.
@@ -52,7 +52,9 @@ module ActiveSupport
def tags_text
tags = current_tags
- if tags.any?
+ if tags.one?
+ "[#{tags[0]}] "
+ elsif tags.any?
tags.collect { |tag| "[#{tag}] " }.join
end
end
diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb
index bc42758f76..f17743b6db 100644
--- a/activesupport/lib/active_support/test_case.rb
+++ b/activesupport/lib/active_support/test_case.rb
@@ -2,16 +2,16 @@
gem "minitest" # make sure we get the gem, not stdlib
require "minitest"
-require_relative "testing/tagged_logging"
-require_relative "testing/setup_and_teardown"
-require_relative "testing/assertions"
-require_relative "testing/deprecation"
-require_relative "testing/declarative"
-require_relative "testing/isolation"
-require_relative "testing/constant_lookup"
-require_relative "testing/time_helpers"
-require_relative "testing/file_fixtures"
-require_relative "core_ext/kernel/reporting"
+require "active_support/testing/tagged_logging"
+require "active_support/testing/setup_and_teardown"
+require "active_support/testing/assertions"
+require "active_support/testing/deprecation"
+require "active_support/testing/declarative"
+require "active_support/testing/isolation"
+require "active_support/testing/constant_lookup"
+require "active_support/testing/time_helpers"
+require "active_support/testing/file_fixtures"
+require "active_support/testing/parallelization"
module ActiveSupport
class TestCase < ::Minitest::Test
@@ -40,12 +40,97 @@ module ActiveSupport
def test_order
ActiveSupport.test_order ||= :random
end
+
+ # Parallelizes the test suite.
+ #
+ # Takes a +workers+ argument that controls how many times the process
+ # is forked. For each process a new database will be created suffixed
+ # with the worker number.
+ #
+ # test-database-0
+ # test-database-1
+ #
+ # If <tt>ENV["PARALLEL_WORKERS"]</tt> is set the workers argument will be ignored
+ # and the environment variable will be used instead. This is useful for CI
+ # environments, or other environments where you may need more workers than
+ # you do for local testing.
+ #
+ # If the number of workers is set to +1+ or fewer, the tests will not be
+ # parallelized.
+ #
+ # The default parallelization method is to fork processes. If you'd like to
+ # use threads instead you can pass <tt>with: :threads</tt> to the +parallelize+
+ # method. Note the threaded parallelization does not create multiple
+ # database and will not work with system tests at this time.
+ #
+ # parallelize(workers: 2, with: :threads)
+ #
+ # The threaded parallelization uses Minitest's parallel executor directly.
+ # The processes parallelization uses a Ruby Drb server.
+ def parallelize(workers: 2, with: :processes)
+ workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
+
+ return if workers <= 1
+
+ executor = case with
+ when :processes
+ Testing::Parallelization.new(workers)
+ when :threads
+ Minitest::Parallel::Executor.new(workers)
+ else
+ raise ArgumentError, "#{with} is not a supported parallelization executor."
+ end
+
+ self.lock_threads = false if defined?(self.lock_threads) && with == :threads
+
+ Minitest.parallel_executor = executor
+
+ parallelize_me!
+ end
+
+ # Set up hook for parallel testing. This can be used if you have multiple
+ # databases or any behavior that needs to be run after the process is forked
+ # but before the tests run.
+ #
+ # Note: this feature is not available with the threaded parallelization.
+ #
+ # In your +test_helper.rb+ add the following:
+ #
+ # class ActiveSupport::TestCase
+ # parallelize_setup do
+ # # create databases
+ # end
+ # end
+ def parallelize_setup(&block)
+ ActiveSupport::Testing::Parallelization.after_fork_hook do |worker|
+ yield worker
+ end
+ end
+
+ # Clean up hook for parallel testing. This can be used to drop databases
+ # if your app uses multiple write/read databases or other clean up before
+ # the tests finish. This runs before the forked process is closed.
+ #
+ # Note: this feature is not available with the threaded parallelization.
+ #
+ # In your +test_helper.rb+ add the following:
+ #
+ # class ActiveSupport::TestCase
+ # parallelize_teardown do
+ # # drop databases
+ # end
+ # end
+ def parallelize_teardown(&block)
+ ActiveSupport::Testing::Parallelization.run_cleanup_hook do |worker|
+ yield worker
+ end
+ end
end
alias_method :method_name, :name
include ActiveSupport::Testing::TaggedLogging
- include ActiveSupport::Testing::SetupAndTeardown
+ prepend ActiveSupport::Testing::SetupAndTeardown
include ActiveSupport::Testing::Assertions
include ActiveSupport::Testing::Deprecation
include ActiveSupport::Testing::TimeHelpers
diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb
index e2bc51ff7a..a891ff616d 100644
--- a/activesupport/lib/active_support/testing/assertions.rb
+++ b/activesupport/lib/active_support/testing/assertions.rb
@@ -58,6 +58,12 @@ module ActiveSupport
# post :create, params: { article: {...} }
# end
#
+ # A hash of expressions/numeric differences can also be passed in and evaluated.
+ #
+ # assert_difference ->{ Article.count } => 1, ->{ Notification.count } => 2 do
+ # post :create, params: { article: {...} }
+ # end
+ #
# A lambda or a list of lambdas can be passed in and evaluated:
#
# assert_difference ->{ Article.count }, 2 do
@@ -73,20 +79,28 @@ module ActiveSupport
# assert_difference 'Article.count', -1, 'An Article should be destroyed' do
# post :delete, params: { id: ... }
# end
- def assert_difference(expression, difference = 1, message = nil, &block)
- expressions = Array(expression)
-
- exps = expressions.map { |e|
+ def assert_difference(expression, *args, &block)
+ expressions =
+ if expression.is_a?(Hash)
+ message = args[0]
+ expression
+ else
+ difference = args[0] || 1
+ message = args[1]
+ Hash[Array(expression).map { |e| [e, difference] }]
+ end
+
+ exps = expressions.keys.map { |e|
e.respond_to?(:call) ? e : lambda { eval(e, block.binding) }
}
before = exps.map(&:call)
retval = yield
- expressions.zip(exps).each_with_index do |(code, e), i|
- error = "#{code.inspect} didn't change by #{difference}"
+ expressions.zip(exps, before) do |(code, diff), exp, before_value|
+ error = "#{code.inspect} didn't change by #{diff}"
error = "#{message}.\n#{error}" if message
- assert_equal(before[i] + difference, e.call, error)
+ assert_equal(before_value + diff, exp.call, error)
end
retval
@@ -156,11 +170,12 @@ module ActiveSupport
after = exp.call
- if to == UNTRACKED
- error = "#{expression.inspect} didn't change"
- error = "#{message}.\n#{error}" if message
- assert_not_equal before, after, error
- else
+ error = "#{expression.inspect} didn't change"
+ error = "#{error}. It was already #{to}" if before == to
+ error = "#{message}.\n#{error}" if message
+ assert before != after, error
+
+ unless to == UNTRACKED
error = "#{expression.inspect} didn't change to #{to}"
error = "#{message}.\n#{error}" if message
assert to === after, error
@@ -190,7 +205,7 @@ module ActiveSupport
error = "#{expression.inspect} did change to #{after}"
error = "#{message}.\n#{error}" if message
- assert_equal before, after, error
+ assert before == after, error
retval
end
diff --git a/activesupport/lib/active_support/testing/constant_lookup.rb b/activesupport/lib/active_support/testing/constant_lookup.rb
index 0fedd486fb..51167e9237 100644
--- a/activesupport/lib/active_support/testing/constant_lookup.rb
+++ b/activesupport/lib/active_support/testing/constant_lookup.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../concern"
-require_relative "../inflector"
+require "active_support/concern"
+require "active_support/inflector"
module ActiveSupport
module Testing
diff --git a/activesupport/lib/active_support/testing/deprecation.rb b/activesupport/lib/active_support/testing/deprecation.rb
index 4fda4832cc..f655435729 100644
--- a/activesupport/lib/active_support/testing/deprecation.rb
+++ b/activesupport/lib/active_support/testing/deprecation.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../deprecation"
-require_relative "../core_ext/regexp"
+require "active_support/deprecation"
+require "active_support/core_ext/regexp"
module ActiveSupport
module Testing
diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb
index 954197a3cc..652a10da23 100644
--- a/activesupport/lib/active_support/testing/isolation.rb
+++ b/activesupport/lib/active_support/testing/isolation.rb
@@ -45,7 +45,8 @@ module ActiveSupport
end
}
end
- result = Marshal.dump(dup)
+ test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
+ result = Marshal.dump(test_result)
end
write.puts [result].pack("m")
@@ -55,7 +56,7 @@ module ActiveSupport
write.close
result = read.read
Process.wait2(pid)
- return result.unpack("m")[0]
+ result.unpack1("m")
end
end
@@ -69,8 +70,9 @@ module ActiveSupport
if ENV["ISOLATION_TEST"]
yield
+ test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
File.open(ENV["ISOLATION_OUTPUT"], "w") do |file|
- file.puts [Marshal.dump(dup)].pack("m")
+ file.puts [Marshal.dump(test_result)].pack("m")
end
exit!
else
@@ -96,7 +98,7 @@ module ActiveSupport
nil
end
- return tmpfile.read.unpack("m")[0]
+ return tmpfile.read.unpack1("m")
end
end
end
diff --git a/activesupport/lib/active_support/testing/parallelization.rb b/activesupport/lib/active_support/testing/parallelization.rb
new file mode 100644
index 0000000000..59c8486f41
--- /dev/null
+++ b/activesupport/lib/active_support/testing/parallelization.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require "drb"
+require "drb/unix"
+
+module ActiveSupport
+ module Testing
+ class Parallelization # :nodoc:
+ class Server
+ include DRb::DRbUndumped
+
+ def initialize
+ @queue = Queue.new
+ end
+
+ def record(reporter, result)
+ reporter.synchronize do
+ reporter.record(result)
+ end
+ end
+
+ def <<(o)
+ @queue << o
+ end
+
+ def pop; @queue.pop; end
+ end
+
+ @after_fork_hooks = []
+
+ def self.after_fork_hook(&blk)
+ @after_fork_hooks << blk
+ end
+
+ def self.after_fork_hooks
+ @after_fork_hooks
+ end
+
+ @run_cleanup_hooks = []
+
+ def self.run_cleanup_hook(&blk)
+ @run_cleanup_hooks << blk
+ end
+
+ def self.run_cleanup_hooks
+ @run_cleanup_hooks
+ end
+
+ def initialize(queue_size)
+ @queue_size = queue_size
+ @queue = Server.new
+ @pool = []
+
+ @url = DRb.start_service("drbunix:", @queue).uri
+ end
+
+ def after_fork(worker)
+ self.class.after_fork_hooks.each do |cb|
+ cb.call(worker)
+ end
+ end
+
+ def run_cleanup(worker)
+ self.class.run_cleanup_hooks.each do |cb|
+ cb.call(worker)
+ end
+ end
+
+ def start
+ @pool = @queue_size.times.map do |worker|
+ fork do
+ DRb.stop_service
+
+ after_fork(worker)
+
+ queue = DRbObject.new_with_uri(@url)
+
+ while job = queue.pop
+ klass = job[0]
+ method = job[1]
+ reporter = job[2]
+ result = Minitest.run_one_method(klass, method)
+
+ queue.record(reporter, result)
+ end
+
+ run_cleanup(worker)
+ end
+ end
+ end
+
+ def <<(work)
+ @queue << work
+ end
+
+ def shutdown
+ @queue_size.times { @queue << nil }
+ @pool.each { |pid| Process.waitpid pid }
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb
index 18de7185d9..35321cd157 100644
--- a/activesupport/lib/active_support/testing/setup_and_teardown.rb
+++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
-require_relative "../concern"
-require_relative "../callbacks"
+require "active_support/callbacks"
module ActiveSupport
module Testing
@@ -19,11 +18,10 @@ module ActiveSupport
# end
# end
module SetupAndTeardown
- extend ActiveSupport::Concern
-
- included do
- include ActiveSupport::Callbacks
- define_callbacks :setup, :teardown
+ def self.prepended(klass)
+ klass.include ActiveSupport::Callbacks
+ klass.define_callbacks :setup, :teardown
+ klass.extend ClassMethods
end
module ClassMethods
@@ -44,7 +42,12 @@ module ActiveSupport
end
def after_teardown # :nodoc:
- run_callbacks :teardown
+ begin
+ run_callbacks :teardown
+ rescue => e
+ self.failures << Minitest::UnexpectedError.new(e)
+ end
+
super
end
end
diff --git a/activesupport/lib/active_support/testing/stream.rb b/activesupport/lib/active_support/testing/stream.rb
index d070a1793d..127cfe1e12 100644
--- a/activesupport/lib/active_support/testing/stream.rb
+++ b/activesupport/lib/active_support/testing/stream.rb
@@ -33,7 +33,7 @@ module ActiveSupport
yield
stream_io.rewind
- return captured_stream.read
+ captured_stream.read
ensure
captured_stream.close
captured_stream.unlink
diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb
index fa5f46736c..801ea2909b 100644
--- a/activesupport/lib/active_support/testing/time_helpers.rb
+++ b/activesupport/lib/active_support/testing/time_helpers.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../core_ext/string/strip" # for strip_heredoc
-require_relative "../core_ext/time/calculations"
+require "active_support/core_ext/module/redefine_method"
+require "active_support/core_ext/time/calculations"
require "concurrent/map"
module ActiveSupport
@@ -43,7 +43,7 @@ module ActiveSupport
def unstub_object(stub)
singleton_class = stub.object.singleton_class
- singleton_class.send :undef_method, stub.method_name
+ singleton_class.send :silence_redefinition_of_method, stub.method_name
singleton_class.send :alias_method, stub.method_name, stub.original_method
singleton_class.send :undef_method, stub.original_method
end
@@ -111,7 +111,7 @@ module ActiveSupport
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
def travel_to(date_or_time)
if block_given? && simple_stubs.stubbing(Time, :now)
- travel_to_nested_block_call = <<-MSG.strip_heredoc
+ travel_to_nested_block_call = <<~MSG
Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing.
diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb
index fb1c5cee1c..51854675bf 100644
--- a/activesupport/lib/active_support/time.rb
+++ b/activesupport/lib/active_support/time.rb
@@ -9,12 +9,12 @@ end
require "date"
require "time"
-require_relative "core_ext/time"
-require_relative "core_ext/date"
-require_relative "core_ext/date_time"
+require "active_support/core_ext/time"
+require "active_support/core_ext/date"
+require "active_support/core_ext/date_time"
-require_relative "core_ext/integer/time"
-require_relative "core_ext/numeric/time"
+require "active_support/core_ext/integer/time"
+require "active_support/core_ext/numeric/time"
-require_relative "core_ext/string/conversions"
-require_relative "core_ext/string/zones"
+require "active_support/core_ext/string/conversions"
+require "active_support/core_ext/string/zones"
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index 59cafa193e..7e71318404 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-require_relative "duration"
-require_relative "values/time_zone"
-require_relative "core_ext/object/acts_like"
-require_relative "core_ext/date_and_time/compatibility"
+require "active_support/duration"
+require "active_support/values/time_zone"
+require "active_support/core_ext/object/acts_like"
+require "active_support/core_ext/date_and_time/compatibility"
module ActiveSupport
# A Time-like class that can represent a time in any time zone. Necessary
@@ -225,6 +225,8 @@ module ActiveSupport
def <=>(other)
utc <=> other
end
+ alias_method :before?, :<
+ alias_method :after?, :>
# Returns true if the current object's time is within the specified
# +min+ and +max+ time.
diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb
index c871f11422..5f709c5fd9 100644
--- a/activesupport/lib/active_support/values/time_zone.rb
+++ b/activesupport/lib/active_support/values/time_zone.rb
@@ -2,7 +2,6 @@
require "tzinfo"
require "concurrent/map"
-require_relative "../core_ext/object/blank"
module ActiveSupport
# The TimeZone class serves as a wrapper around TZInfo::Timezone instances.
@@ -30,7 +29,7 @@ module ActiveSupport
class TimeZone
# Keys are Rails TimeZone names, values are TZInfo identifiers.
MAPPING = {
- "International Date Line West" => "Pacific/Midway",
+ "International Date Line West" => "Etc/GMT+12",
"Midway Island" => "Pacific/Midway",
"American Samoa" => "Pacific/Pago_Pago",
"Hawaii" => "Pacific/Honolulu",
@@ -238,7 +237,7 @@ module ActiveSupport
when Numeric, ActiveSupport::Duration
arg *= 3600 if arg.abs <= 13
all.find { |z| z.utc_offset == arg.to_i }
- else
+ else
raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}"
end
end
@@ -256,22 +255,32 @@ module ActiveSupport
@country_zones[code] ||= load_country_zones(code)
end
+ def clear #:nodoc:
+ @lazy_zones_map = Concurrent::Map.new
+ @country_zones = Concurrent::Map.new
+ @zones = nil
+ @zones_map = nil
+ end
+
private
def load_country_zones(code)
country = TZInfo::Country.get(code)
country.zone_identifiers.map do |tz_id|
if MAPPING.value?(tz_id)
- self[MAPPING.key(tz_id)]
+ MAPPING.inject([]) do |memo, (key, value)|
+ memo << self[key] if value == tz_id
+ memo
+ end
else
create(tz_id, nil, TZInfo::Timezone.new(tz_id))
end
- end.sort!
+ end.flatten(1).sort!
end
def zones_map
- @zones_map ||= begin
- MAPPING.each_key { |place| self[place] } # load all the zones
- @lazy_zones_map
+ @zones_map ||= MAPPING.each_with_object({}) do |(name, _), zones|
+ timezone = self[name]
+ zones[name] = timezone if timezone
end
end
end
@@ -506,7 +515,7 @@ module ActiveSupport
# Available so that TimeZone instances respond like TZInfo::Timezone
# instances.
def period_for_local(time, dst = true)
- tzinfo.period_for_local(time, dst)
+ tzinfo.period_for_local(time, dst) { |periods| periods.last }
end
def periods_for_local(time) #:nodoc:
diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat
deleted file mode 100644
index f7d9c48bbe..0000000000
--- a/activesupport/lib/active_support/values/unicode_tables.dat
+++ /dev/null
Binary files differ
diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb
index d49ee4508e..e42eee07a3 100644
--- a/activesupport/lib/active_support/xml_mini.rb
+++ b/activesupport/lib/active_support/xml_mini.rb
@@ -3,9 +3,9 @@
require "time"
require "base64"
require "bigdecimal"
-require_relative "core_ext/module/delegation"
-require_relative "core_ext/string/inflections"
-require_relative "core_ext/date_time/calculations"
+require "active_support/core_ext/module/delegation"
+require "active_support/core_ext/string/inflections"
+require "active_support/core_ext/date_time/calculations"
module ActiveSupport
# = XmlMini
@@ -48,10 +48,6 @@ module ActiveSupport
"Array" => "array",
"Hash" => "hash"
}
-
- # No need to map these on Ruby 2.4+
- TYPE_NAMES["Fixnum"] = "integer" unless 0.class == Integer
- TYPE_NAMES["Bignum"] = "integer" unless 0.class == Integer
end
FORMATTING = {
@@ -83,7 +79,7 @@ module ActiveSupport
end,
"boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) },
"string" => Proc.new { |string| string.to_s },
- "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
+ "yaml" => Proc.new { |yaml| YAML.load(yaml) rescue yaml },
"base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) },
"binary" => Proc.new { |bin, entity| _parse_binary(bin, entity) },
"file" => Proc.new { |file, entity| _parse_file(file, entity) }
@@ -199,7 +195,7 @@ module ActiveSupport
if name.is_a?(Module)
name
else
- require_relative "xml_mini/#{name.downcase}"
+ require "active_support/xml_mini/#{name.downcase}"
ActiveSupport.const_get("XmlMini_#{name}")
end
end
diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb
index ecfe389f10..7f94a64016 100644
--- a/activesupport/lib/active_support/xml_mini/jdom.rb
+++ b/activesupport/lib/active_support/xml_mini/jdom.rb
@@ -5,7 +5,7 @@ raise "JRuby is required to use the JDOM backend for XmlMini" unless RUBY_PLATFO
require "jruby"
include Java
-require_relative "../core_ext/object/blank"
+require "active_support/core_ext/object/blank"
java_import javax.xml.parsers.DocumentBuilder unless defined? DocumentBuilder
java_import javax.xml.parsers.DocumentBuilderFactory unless defined? DocumentBuilderFactory
diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb
index b1be38fadf..0b000fea60 100644
--- a/activesupport/lib/active_support/xml_mini/libxml.rb
+++ b/activesupport/lib/active_support/xml_mini/libxml.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "libxml"
-require_relative "../core_ext/object/blank"
+require "active_support/core_ext/object/blank"
require "stringio"
module ActiveSupport
diff --git a/activesupport/lib/active_support/xml_mini/libxmlsax.rb b/activesupport/lib/active_support/xml_mini/libxmlsax.rb
index 3ad494310d..dcf16e6084 100644
--- a/activesupport/lib/active_support/xml_mini/libxmlsax.rb
+++ b/activesupport/lib/active_support/xml_mini/libxmlsax.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "libxml"
-require_relative "../core_ext/object/blank"
+require "active_support/core_ext/object/blank"
require "stringio"
module ActiveSupport
diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb
index 1987330c25..5ee6fc8159 100644
--- a/activesupport/lib/active_support/xml_mini/nokogiri.rb
+++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb
@@ -6,7 +6,7 @@ rescue LoadError => e
$stderr.puts "You don't have nokogiri installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
-require_relative "../core_ext/object/blank"
+require "active_support/core_ext/object/blank"
require "stringio"
module ActiveSupport
diff --git a/activesupport/lib/active_support/xml_mini/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb
index fb57eb8867..b01ed00a14 100644
--- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb
+++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb
@@ -6,7 +6,7 @@ rescue LoadError => e
$stderr.puts "You don't have nokogiri installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
-require_relative "../core_ext/object/blank"
+require "active_support/core_ext/object/blank"
require "stringio"
module ActiveSupport
diff --git a/activesupport/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb
index 5fd8978fc0..32458d5b0d 100644
--- a/activesupport/lib/active_support/xml_mini/rexml.rb
+++ b/activesupport/lib/active_support/xml_mini/rexml.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../core_ext/kernel/reporting"
-require_relative "../core_ext/object/blank"
+require "active_support/core_ext/kernel/reporting"
+require "active_support/core_ext/object/blank"
require "stringio"
module ActiveSupport
diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb
index 3ac11e0fe0..f214898145 100644
--- a/activesupport/test/abstract_unit.rb
+++ b/activesupport/test/abstract_unit.rb
@@ -38,4 +38,8 @@ class ActiveSupport::TestCase
private def jruby_skip(message = "")
skip message if defined?(JRUBY_VERSION)
end
+
+ def frozen_error_class
+ Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError
+ end
end
diff --git a/activesupport/test/array_inquirer_test.rb b/activesupport/test/array_inquirer_test.rb
index d5419b862d..9a260edd8a 100644
--- a/activesupport/test/array_inquirer_test.rb
+++ b/activesupport/test/array_inquirer_test.rb
@@ -9,9 +9,9 @@ class ArrayInquirerTest < ActiveSupport::TestCase
end
def test_individual
- assert @array_inquirer.mobile?
- assert @array_inquirer.tablet?
- assert_not @array_inquirer.desktop?
+ assert_predicate @array_inquirer, :mobile?
+ assert_predicate @array_inquirer, :tablet?
+ assert_not_predicate @array_inquirer, :desktop?
end
def test_any
diff --git a/activesupport/test/benchmarkable_test.rb b/activesupport/test/benchmarkable_test.rb
index 424da0a52c..cb7a69cccf 100644
--- a/activesupport/test/benchmarkable_test.rb
+++ b/activesupport/test/benchmarkable_test.rb
@@ -26,7 +26,7 @@ class BenchmarkableTest < ActiveSupport::TestCase
def test_without_block
assert_raise(LocalJumpError) { benchmark }
- assert buffer.empty?
+ assert_empty buffer
end
def test_defaults
diff --git a/activesupport/test/cache/behaviors.rb b/activesupport/test/cache/behaviors.rb
index cb08a10bba..2d39976be3 100644
--- a/activesupport/test/cache/behaviors.rb
+++ b/activesupport/test/cache/behaviors.rb
@@ -3,7 +3,10 @@
require_relative "behaviors/autoloading_cache_behavior"
require_relative "behaviors/cache_delete_matched_behavior"
require_relative "behaviors/cache_increment_decrement_behavior"
+require_relative "behaviors/cache_instrumentation_behavior"
require_relative "behaviors/cache_store_behavior"
require_relative "behaviors/cache_store_version_behavior"
+require_relative "behaviors/connection_pool_behavior"
require_relative "behaviors/encoded_key_cache_behavior"
+require_relative "behaviors/failure_safety_behavior"
require_relative "behaviors/local_cache_behavior"
diff --git a/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb
index 6f59ce48d2..ed8eba8fc2 100644
--- a/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb
+++ b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb
@@ -7,9 +7,9 @@ module CacheDeleteMatchedBehavior
@cache.write("foo/bar", "baz")
@cache.write("fu/baz", "bar")
@cache.delete_matched(/oo/)
- assert !@cache.exist?("foo")
+ assert_not @cache.exist?("foo")
assert @cache.exist?("fu")
- assert !@cache.exist?("foo/bar")
+ assert_not @cache.exist?("foo/bar")
assert @cache.exist?("fu/baz")
end
end
diff --git a/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb
index 2fa2d7af88..16b7abc679 100644
--- a/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb
+++ b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb
@@ -8,7 +8,9 @@ module CacheIncrementDecrementBehavior
assert_equal 2, @cache.read("foo").to_i
assert_equal 3, @cache.increment("foo")
assert_equal 3, @cache.read("foo").to_i
- assert_nil @cache.increment("bar")
+
+ missing = @cache.increment("bar")
+ assert(missing.nil? || missing == 1)
end
def test_decrement
@@ -18,6 +20,8 @@ module CacheIncrementDecrementBehavior
assert_equal 2, @cache.read("foo").to_i
assert_equal 1, @cache.decrement("foo")
assert_equal 1, @cache.read("foo").to_i
- assert_nil @cache.decrement("bar")
+
+ missing = @cache.decrement("bar")
+ assert(missing.nil? || missing == -1)
end
end
diff --git a/activesupport/test/cache/cache_store_write_multi_test.rb b/activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb
index 5b6fd678c5..4e8ff60eb3 100644
--- a/activesupport/test/cache/cache_store_write_multi_test.rb
+++ b/activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb
@@ -1,28 +1,15 @@
# frozen_string_literal: true
-require "abstract_unit"
-require "active_support/cache"
-
-class CacheStoreWriteMultiEntriesStoreProviderInterfaceTest < ActiveSupport::TestCase
- setup do
- @cache = ActiveSupport::Cache.lookup_store(:null_store)
- end
-
- test "fetch_multi uses write_multi_entries store provider interface" do
+module CacheInstrumentationBehavior
+ def test_fetch_multi_uses_write_multi_entries_store_provider_interface
assert_called_with(@cache, :write_multi_entries) do
@cache.fetch_multi "a", "b", "c" do |key|
key * 2
end
end
end
-end
-class CacheStoreWriteMultiInstrumentationTest < ActiveSupport::TestCase
- setup do
- @cache = ActiveSupport::Cache.lookup_store(:null_store)
- end
-
- test "instrumentation" do
+ def test_write_multi_instrumentation
writes = { "a" => "aa", "b" => "bb" }
events = with_instrumentation "write_multi" do
@@ -34,16 +21,27 @@ class CacheStoreWriteMultiInstrumentationTest < ActiveSupport::TestCase
assert_equal({ "a" => "aa", "b" => "bb" }, events[0].payload[:key])
end
- test "instrumentation with fetch_multi as super operation" do
- skip "fetch_multi isn't instrumented yet"
+ def test_instrumentation_with_fetch_multi_as_super_operation
+ @cache.write("b", "bb")
- events = with_instrumentation "write_multi" do
+ events = with_instrumentation "read_multi" do
@cache.fetch_multi("a", "b") { |key| key * 2 }
end
- assert_equal %w[ cache_write_multi.active_support ], events.map(&:name)
- assert_nil events[0].payload[:super_operation]
- assert !events[0].payload[:hit]
+ assert_equal %w[ cache_read_multi.active_support ], events.map(&:name)
+ assert_equal :fetch_multi, events[0].payload[:super_operation]
+ assert_equal ["b"], events[0].payload[:hits]
+ end
+
+ def test_read_multi_instrumentation
+ @cache.write("b", "bb")
+
+ events = with_instrumentation "read_multi" do
+ @cache.read_multi("a", "b") { |key| key * 2 }
+ end
+
+ assert_equal %w[ cache_read_multi.active_support ], events.map(&:name)
+ assert_equal ["b"], events[0].payload[:hits]
end
private
diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb
index 582e902f72..f9153ffe2a 100644
--- a/activesupport/test/cache/behaviors/cache_store_behavior.rb
+++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb
@@ -33,7 +33,7 @@ module CacheStoreBehavior
cache_miss = false
assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length }
- assert !cache_miss
+ assert_not cache_miss
end
def test_fetch_with_forced_cache_miss
@@ -113,6 +113,16 @@ module CacheStoreBehavior
assert_equal("fufu", @cache.read("fu"))
end
+ def test_fetch_multi_without_expires_in
+ @cache.write("foo", "bar")
+ @cache.write("fud", "biz")
+
+ values = @cache.fetch_multi("foo", "fu", "fud", expires_in: nil) { |value| value * 2 }
+
+ assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values)
+ assert_equal("fufu", @cache.read("fu"))
+ end
+
def test_multi_with_objects
cache_struct = Struct.new(:cache_key, :title)
foo = cache_struct.new("foo", "FOO!")
@@ -131,19 +141,111 @@ module CacheStoreBehavior
end
end
- def test_read_and_write_compressed_small_data
- @cache.write("foo", "bar", compress: true)
- assert_equal "bar", @cache.read("foo")
+ # Use strings that are guarenteed to compress well, so we can easily tell if
+ # the compression kicked in or not.
+ SMALL_STRING = "0" * 100
+ LARGE_STRING = "0" * 2.kilobytes
+
+ SMALL_OBJECT = { data: SMALL_STRING }
+ LARGE_OBJECT = { data: LARGE_STRING }
+
+ def test_nil_with_default_compression_settings
+ assert_uncompressed(nil)
end
- def test_read_and_write_compressed_large_data
- @cache.write("foo", "bar", compress: true, compress_threshold: 2)
- assert_equal "bar", @cache.read("foo")
+ def test_nil_with_compress_true
+ assert_uncompressed(nil, compress: true)
end
- def test_read_and_write_compressed_nil
- @cache.write("foo", nil, compress: true)
- assert_nil @cache.read("foo")
+ def test_nil_with_compress_false
+ assert_uncompressed(nil, compress: false)
+ end
+
+ def test_nil_with_compress_low_compress_threshold
+ assert_uncompressed(nil, compress: true, compress_threshold: 1)
+ end
+
+ def test_small_string_with_default_compression_settings
+ assert_uncompressed(SMALL_STRING)
+ end
+
+ def test_small_string_with_compress_true
+ assert_uncompressed(SMALL_STRING, compress: true)
+ end
+
+ def test_small_string_with_compress_false
+ assert_uncompressed(SMALL_STRING, compress: false)
+ end
+
+ def test_small_string_with_low_compress_threshold
+ assert_compressed(SMALL_STRING, compress: true, compress_threshold: 1)
+ end
+
+ def test_small_object_with_default_compression_settings
+ assert_uncompressed(SMALL_OBJECT)
+ end
+
+ def test_small_object_with_compress_true
+ assert_uncompressed(SMALL_OBJECT, compress: true)
+ end
+
+ def test_small_object_with_compress_false
+ assert_uncompressed(SMALL_OBJECT, compress: false)
+ end
+
+ def test_small_object_with_low_compress_threshold
+ assert_compressed(SMALL_OBJECT, compress: true, compress_threshold: 1)
+ end
+
+ def test_large_string_with_default_compression_settings
+ assert_compressed(LARGE_STRING)
+ end
+
+ def test_large_string_with_compress_true
+ assert_compressed(LARGE_STRING, compress: true)
+ end
+
+ def test_large_string_with_compress_false
+ assert_uncompressed(LARGE_STRING, compress: false)
+ end
+
+ def test_large_string_with_high_compress_threshold
+ assert_uncompressed(LARGE_STRING, compress: true, compress_threshold: 1.megabyte)
+ end
+
+ def test_large_object_with_default_compression_settings
+ assert_compressed(LARGE_OBJECT)
+ end
+
+ def test_large_object_with_compress_true
+ assert_compressed(LARGE_OBJECT, compress: true)
+ end
+
+ def test_large_object_with_compress_false
+ assert_uncompressed(LARGE_OBJECT, compress: false)
+ end
+
+ def test_large_object_with_high_compress_threshold
+ assert_uncompressed(LARGE_OBJECT, compress: true, compress_threshold: 1.megabyte)
+ end
+
+ def test_incompressable_data
+ assert_uncompressed(nil, compress: true, compress_threshold: 1)
+ assert_uncompressed(true, compress: true, compress_threshold: 1)
+ assert_uncompressed(false, compress: true, compress_threshold: 1)
+ assert_uncompressed(0, compress: true, compress_threshold: 1)
+ assert_uncompressed(1.2345, compress: true, compress_threshold: 1)
+ assert_uncompressed("", compress: true, compress_threshold: 1)
+
+ incompressible = nil
+
+ # generate an incompressible string
+ loop do
+ incompressible = SecureRandom.random_bytes(1.kilobyte)
+ break if incompressible.bytesize < Zlib::Deflate.deflate(incompressible).bytesize
+ end
+
+ assert_uncompressed(incompressible, compress: true, compress_threshold: 1)
end
def test_cache_key
@@ -164,6 +266,18 @@ module CacheStoreBehavior
assert_equal "bar", @cache.read("foo")
end
+ def test_unversioned_cache_key
+ obj = Object.new
+ def obj.cache_key
+ "foo"
+ end
+ def obj.cache_key_with_version
+ "foo-v1"
+ end
+ @cache.write(obj, "bar")
+ assert_equal "bar", @cache.read("foo")
+ end
+
def test_array_as_cache_key
@cache.write([:fu, "foo"], "bar")
assert_equal "bar", @cache.read("fu/foo")
@@ -194,7 +308,7 @@ module CacheStoreBehavior
@cache.write("foo", "bar")
assert @cache.exist?("foo")
assert @cache.delete("foo")
- assert !@cache.exist?("foo")
+ assert_not @cache.exist?("foo")
end
def test_original_store_objects_should_not_be_immutable
@@ -287,8 +401,7 @@ module CacheStoreBehavior
end
def test_really_long_keys
- key = "".dup
- 900.times { key << "x" }
+ key = "x" * 2048
assert @cache.write(key, "bar")
assert_equal "bar", @cache.read(key)
assert_equal "bar", @cache.fetch(key)
@@ -328,4 +441,41 @@ module CacheStoreBehavior
ensure
ActiveSupport::Notifications.unsubscribe "cache_read.active_support"
end
+
+ private
+
+ def assert_compressed(value, **options)
+ assert_compression(true, value, **options)
+ end
+
+ def assert_uncompressed(value, **options)
+ assert_compression(false, value, **options)
+ end
+
+ def assert_compression(should_compress, value, **options)
+ freeze_time do
+ @cache.write("actual", value, options)
+ @cache.write("uncompressed", value, options.merge(compress: false))
+ end
+
+ if value.nil?
+ assert_nil @cache.read("actual")
+ assert_nil @cache.read("uncompressed")
+ else
+ assert_equal value, @cache.read("actual")
+ assert_equal value, @cache.read("uncompressed")
+ end
+
+ actual_entry = @cache.send(:read_entry, @cache.send(:normalize_key, "actual", {}), {})
+ uncompressed_entry = @cache.send(:read_entry, @cache.send(:normalize_key, "uncompressed", {}), {})
+
+ actual_size = Marshal.dump(actual_entry).bytesize
+ uncompressed_size = Marshal.dump(uncompressed_entry).bytesize
+
+ if should_compress
+ assert_operator actual_size, :<, uncompressed_size, "value should be compressed"
+ else
+ assert_equal uncompressed_size, actual_size, "value should not be compressed"
+ end
+ end
end
diff --git a/activesupport/test/cache/behaviors/cache_store_version_behavior.rb b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb
index c2e4d046af..805f061839 100644
--- a/activesupport/test/cache/behaviors/cache_store_version_behavior.rb
+++ b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb
@@ -30,7 +30,7 @@ module CacheStoreVersionBehavior
def test_exist_with_wrong_version_should_be_false
@cache.write("foo", "bar", version: 1)
- assert !@cache.exist?("foo", version: 2)
+ assert_not @cache.exist?("foo", version: 2)
end
def test_reading_and_writing_with_model_supporting_cache_version
@@ -65,7 +65,7 @@ module CacheStoreVersionBehavior
m1v2 = ModelWithKeyAndVersion.new("model/1", 2)
@cache.write(m1v1, "bar")
- assert @cache.exist?(m1v1)
+ assert @cache.exist?(m1v1)
assert_not @cache.fetch(m1v2)
end
diff --git a/activesupport/test/cache/behaviors/connection_pool_behavior.rb b/activesupport/test/cache/behaviors/connection_pool_behavior.rb
new file mode 100644
index 0000000000..701cd75595
--- /dev/null
+++ b/activesupport/test/cache/behaviors/connection_pool_behavior.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module ConnectionPoolBehavior
+ def test_connection_pool
+ Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception
+
+ emulating_latency do
+ begin
+ cache = ActiveSupport::Cache.lookup_store(store, { pool_size: 2, pool_timeout: 1 }.merge(store_options))
+ cache.clear
+
+ threads = []
+
+ assert_raises Timeout::Error do
+ # One of the three threads will fail in 1 second because our pool size
+ # is only two.
+ 3.times do
+ threads << Thread.new do
+ cache.read("latency")
+ end
+ end
+
+ threads.each(&:join)
+ end
+ ensure
+ threads.each(&:kill)
+ end
+ end
+ ensure
+ Thread.report_on_exception = original_report_on_exception
+ end
+
+ def test_no_connection_pool
+ emulating_latency do
+ begin
+ cache = ActiveSupport::Cache.lookup_store(store, store_options)
+ cache.clear
+
+ threads = []
+
+ assert_nothing_raised do
+ # Default connection pool size is 5, assuming 10 will make sure that
+ # the connection pool isn't used at all.
+ 10.times do
+ threads << Thread.new do
+ cache.read("latency")
+ end
+ end
+
+ threads.each(&:join)
+ end
+ ensure
+ threads.each(&:kill)
+ end
+ end
+ end
+
+ private
+ def store_options; {}; end
+end
diff --git a/activesupport/test/cache/behaviors/failure_safety_behavior.rb b/activesupport/test/cache/behaviors/failure_safety_behavior.rb
new file mode 100644
index 0000000000..43b67d81db
--- /dev/null
+++ b/activesupport/test/cache/behaviors/failure_safety_behavior.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module FailureSafetyBehavior
+ def test_fetch_read_failure_returns_nil
+ @cache.write("foo", "bar")
+
+ emulating_unavailability do |cache|
+ assert_nil cache.fetch("foo")
+ end
+ end
+
+ def test_fetch_read_failure_does_not_attempt_to_write
+ end
+
+ def test_read_failure_returns_nil
+ @cache.write("foo", "bar")
+
+ emulating_unavailability do |cache|
+ assert_nil cache.read("foo")
+ end
+ end
+
+ def test_read_multi_failure_returns_empty_hash
+ @cache.write_multi("foo" => "bar", "baz" => "quux")
+
+ emulating_unavailability do |cache|
+ assert_equal Hash.new, cache.read_multi("foo", "baz")
+ end
+ end
+
+ def test_write_failure_returns_false
+ emulating_unavailability do |cache|
+ assert_equal false, cache.write("foo", "bar")
+ end
+ end
+
+ def test_write_multi_failure_not_raises
+ emulating_unavailability do |cache|
+ assert_nothing_raised do
+ cache.write_multi("foo" => "bar", "baz" => "quux")
+ end
+ end
+ end
+
+ def test_fetch_multi_failure_returns_fallback_results
+ @cache.write_multi("foo" => "bar", "baz" => "quux")
+
+ emulating_unavailability do |cache|
+ fetched = cache.fetch_multi("foo", "baz") { |k| "unavailable" }
+ assert_equal Hash["foo" => "unavailable", "baz" => "unavailable"], fetched
+ end
+ end
+
+ def test_delete_failure_returns_false
+ @cache.write("foo", "bar")
+
+ emulating_unavailability do |cache|
+ assert_equal false, cache.delete("foo")
+ end
+ end
+
+ def test_exist_failure_returns_false
+ @cache.write("foo", "bar")
+
+ emulating_unavailability do |cache|
+ assert_not cache.exist?("foo")
+ end
+ end
+
+ def test_increment_failure_returns_nil
+ @cache.write("foo", 1, raw: true)
+
+ emulating_unavailability do |cache|
+ assert_nil cache.increment("foo")
+ end
+ end
+
+ def test_decrement_failure_returns_nil
+ @cache.write("foo", 1, raw: true)
+
+ emulating_unavailability do |cache|
+ assert_nil cache.decrement("foo")
+ end
+ end
+
+ def test_clear_failure_returns_nil
+ emulating_unavailability do |cache|
+ assert_nil cache.clear
+ end
+ end
+end
diff --git a/activesupport/test/cache/behaviors/local_cache_behavior.rb b/activesupport/test/cache/behaviors/local_cache_behavior.rb
index 8dec8090b1..baa38ba6ac 100644
--- a/activesupport/test/cache/behaviors/local_cache_behavior.rb
+++ b/activesupport/test/cache/behaviors/local_cache_behavior.rb
@@ -20,7 +20,11 @@ module LocalCacheBehavior
end
def test_cleanup_clears_local_cache_but_not_remote_cache
- skip unless @cache.class.instance_methods(false).include?(:cleanup)
+ begin
+ @cache.cleanup
+ rescue NotImplementedError
+ skip
+ end
@cache.with_local_cache do
@cache.write("foo", "bar")
@@ -115,6 +119,28 @@ module LocalCacheBehavior
end
end
+ def test_local_cache_of_fetch_multi
+ @cache.with_local_cache do
+ @cache.fetch_multi("foo", "bar") { |_key| true }
+ @peek.delete("foo")
+ @peek.delete("bar")
+ assert_equal true, @cache.read("foo")
+ assert_equal true, @cache.read("bar")
+ end
+ end
+
+ def test_local_cache_of_read_multi
+ @cache.with_local_cache do
+ @cache.write("foo", "foo", raw: true)
+ @cache.write("bar", "bar", raw: true)
+ values = @cache.read_multi("foo", "bar")
+ assert_equal "foo", @cache.read("foo")
+ assert_equal "bar", @cache.read("bar")
+ assert_equal "foo", values["foo"]
+ assert_equal "bar", values["bar"]
+ end
+ end
+
def test_middleware
app = lambda { |env|
result = @cache.write("foo", "bar")
diff --git a/activesupport/test/cache/cache_entry_test.rb b/activesupport/test/cache/cache_entry_test.rb
index 51b214ad8f..d7baaa5c72 100644
--- a/activesupport/test/cache/cache_entry_test.rb
+++ b/activesupport/test/cache/cache_entry_test.rb
@@ -13,18 +13,4 @@ class CacheEntryTest < ActiveSupport::TestCase
assert entry.expired?, "entry is expired"
end
end
-
- def test_compress_values
- value = "value" * 100
- entry = ActiveSupport::Cache::Entry.new(value, compress: true, compress_threshold: 1)
- assert_equal value, entry.value
- assert(value.bytesize > entry.size, "value is compressed")
- end
-
- def test_non_compress_values
- value = "value" * 100
- entry = ActiveSupport::Cache::Entry.new(value)
- assert_equal value, entry.value
- assert_equal value.bytesize, entry.size
- end
end
diff --git a/activesupport/test/cache/cache_store_logger_test.rb b/activesupport/test/cache/cache_store_logger_test.rb
index 1af6893cc9..4648b2d361 100644
--- a/activesupport/test/cache/cache_store_logger_test.rb
+++ b/activesupport/test/cache/cache_store_logger_test.rb
@@ -13,7 +13,7 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase
def test_logging
@cache.fetch("foo") { "bar" }
- assert @buffer.string.present?
+ assert_predicate @buffer.string, :present?
end
def test_log_with_string_namespace
@@ -31,6 +31,6 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase
def test_mute_logging
@cache.mute { @cache.fetch("foo") { "bar" } }
- assert @buffer.string.blank?
+ assert_predicate @buffer.string, :blank?
end
end
diff --git a/activesupport/test/cache/cache_store_namespace_test.rb b/activesupport/test/cache/cache_store_namespace_test.rb
index b52a61c500..dfdb3262f2 100644
--- a/activesupport/test/cache/cache_store_namespace_test.rb
+++ b/activesupport/test/cache/cache_store_namespace_test.rb
@@ -25,7 +25,7 @@ class CacheStoreNamespaceTest < ActiveSupport::TestCase
cache.write("foo", "bar")
cache.write("fu", "baz")
cache.delete_matched(/^fo/)
- assert !cache.exist?("foo")
+ assert_not cache.exist?("foo")
assert cache.exist?("fu")
end
@@ -34,7 +34,7 @@ class CacheStoreNamespaceTest < ActiveSupport::TestCase
cache.write("foo", "bar")
cache.write("fu", "baz")
cache.delete_matched(/OO/i)
- assert !cache.exist?("foo")
+ assert_not cache.exist?("foo")
assert cache.exist?("fu")
end
end
diff --git a/activesupport/test/cache/stores/file_store_test.rb b/activesupport/test/cache/stores/file_store_test.rb
index 391ab60b3a..f6855bb308 100644
--- a/activesupport/test/cache/stores/file_store_test.rb
+++ b/activesupport/test/cache/stores/file_store_test.rb
@@ -30,6 +30,7 @@ class FileStoreTest < ActiveSupport::TestCase
include LocalCacheBehavior
include CacheDeleteMatchedBehavior
include CacheIncrementDecrementBehavior
+ include CacheInstrumentationBehavior
include AutoloadingCacheBehavior
def test_clear
@@ -67,7 +68,9 @@ class FileStoreTest < ActiveSupport::TestCase
def test_filename_max_size
key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}"
path = @cache.send(:normalize_key, key, {})
- Dir::Tmpname.create(path) do |tmpname, n, opts|
+ basename = File.basename(path)
+ dirname = File.dirname(path)
+ Dir::Tmpname.create(basename, Dir.tmpdir + dirname) do |tmpname, n, opts|
assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}"
end
end
@@ -98,13 +101,13 @@ class FileStoreTest < ActiveSupport::TestCase
end
assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!"
assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!"
- assert Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }.empty?
+ assert_empty Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }
end
def test_log_exception_when_cache_read_fails
File.stub(:exist?, -> { raise StandardError.new("failed") }) do
@cache.send(:read_entry, "winston", {})
- assert @buffer.string.present?
+ assert_predicate @buffer.string, :present?
end
end
@@ -118,6 +121,7 @@ class FileStoreTest < ActiveSupport::TestCase
assert_not @cache.exist?("foo")
assert @cache.exist?("baz")
assert @cache.exist?("quux")
+ assert_equal 2, Dir.glob(File.join(cache_dir, "**")).size
end
end
diff --git a/activesupport/test/cache/stores/mem_cache_store_test.rb b/activesupport/test/cache/stores/mem_cache_store_test.rb
index 1b73fb65eb..f426a37c66 100644
--- a/activesupport/test/cache/stores/mem_cache_store_test.rb
+++ b/activesupport/test/cache/stores/mem_cache_store_test.rb
@@ -5,6 +5,24 @@ require "active_support/cache"
require_relative "../behaviors"
require "dalli"
+# Emulates a latency on Dalli's back-end for the key latency to facilitate
+# connection pool testing.
+class SlowDalliClient < Dalli::Client
+ def get(key, options = {})
+ if key =~ /latency/
+ sleep 3
+ else
+ super
+ end
+ end
+end
+
+class UnavailableDalliServer < Dalli::Server
+ def alive?
+ false
+ end
+end
+
class MemCacheStoreTest < ActiveSupport::TestCase
begin
ss = Dalli::Client.new("localhost:11211").stats
@@ -31,8 +49,11 @@ class MemCacheStoreTest < ActiveSupport::TestCase
include CacheStoreVersionBehavior
include LocalCacheBehavior
include CacheIncrementDecrementBehavior
+ include CacheInstrumentationBehavior
include EncodedKeyCacheBehavior
include AutoloadingCacheBehavior
+ include ConnectionPoolBehavior
+ include FailureSafetyBehavior
def test_raw_values
cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
@@ -57,6 +78,22 @@ class MemCacheStoreTest < ActiveSupport::TestCase
end
end
+ def test_increment_expires_in
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
+ cache.clear
+ assert_called_with cache.instance_variable_get(:@data), :incr, [ "foo", 1, 60 ] do
+ cache.increment("foo", 1, expires_in: 60)
+ end
+ end
+
+ def test_decrement_expires_in
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
+ cache.clear
+ assert_called_with cache.instance_variable_get(:@data), :decr, [ "foo", 1, 60 ] do
+ cache.decrement("foo", 1, expires_in: 60)
+ end
+ end
+
def test_local_cache_raw_values_with_marshal
cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
cache.clear
@@ -73,4 +110,30 @@ class MemCacheStoreTest < ActiveSupport::TestCase
value << "bingo"
assert_not_equal value, @cache.read("foo")
end
+
+ private
+
+ def store
+ :mem_cache_store
+ end
+
+ def emulating_latency
+ old_client = Dalli.send(:remove_const, :Client)
+ Dalli.const_set(:Client, SlowDalliClient)
+
+ yield
+ ensure
+ Dalli.send(:remove_const, :Client)
+ Dalli.const_set(:Client, old_client)
+ end
+
+ def emulating_unavailability
+ old_server = Dalli.send(:remove_const, :Server)
+ Dalli.const_set(:Server, UnavailableDalliServer)
+
+ yield ActiveSupport::Cache::MemCacheStore.new
+ ensure
+ Dalli.send(:remove_const, :Server)
+ Dalli.const_set(:Server, old_server)
+ end
end
diff --git a/activesupport/test/cache/stores/memory_store_test.rb b/activesupport/test/cache/stores/memory_store_test.rb
index 3981f05331..340fb517cb 100644
--- a/activesupport/test/cache/stores/memory_store_test.rb
+++ b/activesupport/test/cache/stores/memory_store_test.rb
@@ -6,14 +6,21 @@ require_relative "../behaviors"
class MemoryStoreTest < ActiveSupport::TestCase
def setup
- @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa"))
- @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1)
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60)
end
include CacheStoreBehavior
include CacheStoreVersionBehavior
include CacheDeleteMatchedBehavior
include CacheIncrementDecrementBehavior
+ include CacheInstrumentationBehavior
+end
+
+class MemoryStorePruningTest < ActiveSupport::TestCase
+ def setup
+ @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa"))
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1)
+ end
def test_prune_size
@cache.write(1, "aaaaaaaaaa") && sleep(0.001)
@@ -97,7 +104,7 @@ class MemoryStoreTest < ActiveSupport::TestCase
assert @cache.exist?(4)
assert @cache.exist?(3)
assert @cache.exist?(2)
- assert !@cache.exist?(1)
+ assert_not @cache.exist?(1)
end
def test_write_with_unless_exist
diff --git a/activesupport/test/cache/stores/redis_cache_store_test.rb b/activesupport/test/cache/stores/redis_cache_store_test.rb
new file mode 100644
index 0000000000..24c4c5c481
--- /dev/null
+++ b/activesupport/test/cache/stores/redis_cache_store_test.rb
@@ -0,0 +1,242 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/cache"
+require "active_support/cache/redis_cache_store"
+require_relative "../behaviors"
+
+driver_name = %w[ ruby hiredis ].include?(ENV["REDIS_DRIVER"]) ? ENV["REDIS_DRIVER"] : "hiredis"
+driver = Object.const_get("Redis::Connection::#{driver_name.camelize}")
+
+Redis::Connection.drivers.clear
+Redis::Connection.drivers.append(driver)
+
+# Emulates a latency on Redis's back-end for the key latency to facilitate
+# connection pool testing.
+class SlowRedis < Redis
+ def get(key, options = {})
+ if key =~ /latency/
+ sleep 3
+ else
+ super
+ end
+ end
+end
+
+module ActiveSupport::Cache::RedisCacheStoreTests
+ DRIVER = %w[ ruby hiredis ].include?(ENV["REDIS_DRIVER"]) ? ENV["REDIS_DRIVER"] : "hiredis"
+
+ class LookupTest < ActiveSupport::TestCase
+ test "may be looked up as :redis_cache_store" do
+ assert_kind_of ActiveSupport::Cache::RedisCacheStore,
+ ActiveSupport::Cache.lookup_store(:redis_cache_store)
+ end
+ end
+
+ class InitializationTest < ActiveSupport::TestCase
+ test "omitted URL uses Redis client with default settings" do
+ assert_called_with Redis, :new, [
+ url: nil,
+ connect_timeout: 20, read_timeout: 1, write_timeout: 1,
+ reconnect_attempts: 0, driver: DRIVER
+ ] do
+ build
+ end
+ end
+
+ test "no URLs uses Redis client with default settings" do
+ assert_called_with Redis, :new, [
+ url: nil,
+ connect_timeout: 20, read_timeout: 1, write_timeout: 1,
+ reconnect_attempts: 0, driver: DRIVER
+ ] do
+ build url: []
+ end
+ end
+
+ test "singular URL uses Redis client" do
+ assert_called_with Redis, :new, [
+ url: "redis://localhost:6379/0",
+ connect_timeout: 20, read_timeout: 1, write_timeout: 1,
+ reconnect_attempts: 0, driver: DRIVER
+ ] do
+ build url: "redis://localhost:6379/0"
+ end
+ end
+
+ test "one URL uses Redis client" do
+ assert_called_with Redis, :new, [
+ url: "redis://localhost:6379/0",
+ connect_timeout: 20, read_timeout: 1, write_timeout: 1,
+ reconnect_attempts: 0, driver: DRIVER
+ ] do
+ build url: %w[ redis://localhost:6379/0 ]
+ end
+ end
+
+ test "multiple URLs uses Redis::Distributed client" do
+ assert_called_with Redis, :new, [
+ [ url: "redis://localhost:6379/0",
+ connect_timeout: 20, read_timeout: 1, write_timeout: 1,
+ reconnect_attempts: 0, driver: DRIVER ],
+ [ url: "redis://localhost:6379/1",
+ connect_timeout: 20, read_timeout: 1, write_timeout: 1,
+ reconnect_attempts: 0, driver: DRIVER ],
+ ], returns: Redis.new do
+ @cache = build url: %w[ redis://localhost:6379/0 redis://localhost:6379/1 ]
+ assert_kind_of ::Redis::Distributed, @cache.redis
+ end
+ end
+
+ test "block argument uses yielded client" do
+ block = -> { :custom_redis_client }
+ assert_called block, :call do
+ build redis: block
+ end
+ end
+
+ test "instance of Redis uses given instance" do
+ redis_instance = Redis.new
+ @cache = build(redis: redis_instance)
+ assert_same @cache.redis, redis_instance
+ end
+
+ private
+ def build(**kwargs)
+ ActiveSupport::Cache::RedisCacheStore.new(driver: DRIVER, **kwargs).tap do |cache|
+ cache.redis
+ end
+ end
+ end
+
+ class StoreTest < ActiveSupport::TestCase
+ setup do
+ @namespace = "namespace"
+
+ @cache = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, namespace: @namespace, expires_in: 60, driver: DRIVER)
+ # @cache.logger = Logger.new($stdout) # For test debugging
+
+ # For LocalCacheBehavior tests
+ @peek = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, namespace: @namespace, driver: DRIVER)
+ end
+
+ teardown do
+ @cache.clear
+ @cache.redis.disconnect!
+ end
+ end
+
+ class RedisCacheStoreCommonBehaviorTest < StoreTest
+ include CacheStoreBehavior
+ include CacheStoreVersionBehavior
+ include LocalCacheBehavior
+ include CacheIncrementDecrementBehavior
+ include CacheInstrumentationBehavior
+ include AutoloadingCacheBehavior
+
+ def test_fetch_multi_uses_redis_mget
+ assert_called(@cache.redis, :mget, returns: []) do
+ @cache.fetch_multi("a", "b", "c") do |key|
+ key * 2
+ end
+ end
+ end
+ end
+
+ class ConnectionPoolBehaviourTest < StoreTest
+ include ConnectionPoolBehavior
+
+ private
+
+ def store
+ :redis_cache_store
+ end
+
+ def emulating_latency
+ old_redis = Object.send(:remove_const, :Redis)
+ Object.const_set(:Redis, SlowRedis)
+
+ yield
+ ensure
+ Object.send(:remove_const, :Redis)
+ Object.const_set(:Redis, old_redis)
+ end
+ end
+
+ class RedisDistributedConnectionPoolBehaviourTest < ConnectionPoolBehaviourTest
+ private
+ def store_options
+ { url: %w[ redis://localhost:6379/0 redis://localhost:6379/0 ] }
+ end
+ end
+
+ # Separate test class so we can omit the namespace which causes expected,
+ # appropriate complaints about incompatible string encodings.
+ class KeyEncodingSafetyTest < StoreTest
+ include EncodedKeyCacheBehavior
+
+ setup do
+ @cache = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, driver: DRIVER)
+ @cache.logger = nil
+ end
+ end
+
+ class StoreAPITest < StoreTest
+ end
+
+ class UnavailableRedisClient < Redis::Client
+ def ensure_connected
+ raise Redis::BaseConnectionError
+ end
+ end
+
+ class FailureSafetyTest < StoreTest
+ include FailureSafetyBehavior
+
+ private
+
+ def emulating_unavailability
+ old_client = Redis.send(:remove_const, :Client)
+ Redis.const_set(:Client, UnavailableRedisClient)
+
+ yield ActiveSupport::Cache::RedisCacheStore.new
+ ensure
+ Redis.send(:remove_const, :Client)
+ Redis.const_set(:Client, old_client)
+ end
+ end
+
+ class DeleteMatchedTest < StoreTest
+ test "deletes keys matching glob" do
+ @cache.write("foo", "bar")
+ @cache.write("fu", "baz")
+ @cache.delete_matched("foo*")
+ assert_not @cache.exist?("foo")
+ assert @cache.exist?("fu")
+ end
+
+ test "fails with regexp matchers" do
+ assert_raise ArgumentError do
+ @cache.delete_matched(/OO/i)
+ end
+ end
+ end
+
+ class ClearTest < StoreTest
+ test "clear all cache key" do
+ @cache.write("foo", "bar")
+ @cache.write("fu", "baz")
+ @cache.clear
+ assert_not @cache.exist?("foo")
+ assert_not @cache.exist?("fu")
+ end
+
+ test "only clear namespace cache key" do
+ @cache.write("foo", "bar")
+ @cache.redis.set("fu", "baz")
+ @cache.clear
+ assert_not @cache.exist?("foo")
+ assert @cache.redis.exists("fu")
+ end
+ end
+end
diff --git a/activesupport/test/callback_inheritance_test.rb b/activesupport/test/callback_inheritance_test.rb
index 67813a749e..5633b6e2b8 100644
--- a/activesupport/test/callback_inheritance_test.rb
+++ b/activesupport/test/callback_inheritance_test.rb
@@ -164,10 +164,10 @@ end
class DynamicInheritedCallbacks < ActiveSupport::TestCase
def test_callbacks_looks_to_the_superclass_before_running
child = EmptyChild.new.dispatch
- assert !child.performed?
+ assert_not_predicate child, :performed?
EmptyParent.set_callback :dispatch, :before, :perform!
child = EmptyChild.new.dispatch
- assert child.performed?
+ assert_predicate child, :performed?
end
def test_callbacks_should_be_performed_once_in_child_class
@@ -176,3 +176,13 @@ class DynamicInheritedCallbacks < ActiveSupport::TestCase
assert_equal 1, child.count
end
end
+
+class DynamicDefinedCallbacks < ActiveSupport::TestCase
+ def test_callbacks_should_be_performed_once_in_child_class_after_dynamic_define
+ GrandParent.define_callbacks(:foo)
+ GrandParent.set_callback(:foo, :before, :before1)
+ parent = Parent.new("foo")
+ parent.run_callbacks(:foo)
+ assert_equal %w(before1), parent.log
+ end
+end
diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb
index 3902e41a60..5c9a3b29e7 100644
--- a/activesupport/test/callbacks_test.rb
+++ b/activesupport/test/callbacks_test.rb
@@ -77,7 +77,7 @@ module CallbacksTest
skip_callback :save, :after, :after_save_method, unless: :yes
skip_callback :save, :after, :after_save_method, if: :no
skip_callback :save, :before, :before_save_method, unless: :no
- skip_callback :save, :before, CallbackClass , if: :yes
+ skip_callback :save, :before, CallbackClass, if: :yes
def yes; true; end
def no; false; end
end
@@ -193,13 +193,6 @@ module CallbacksTest
before_save Proc.new { |r| r.history << "b00m" }, if: :no
before_save Proc.new { |r| r.history << [:before_save, :symbol] }, unless: :no
before_save Proc.new { |r| r.history << "b00m" }, unless: :yes
- # string
- ActiveSupport::Deprecation.silence do
- before_save Proc.new { |r| r.history << [:before_save, :string] }, if: "yes"
- before_save Proc.new { |r| r.history << "b00m" }, if: "no"
- before_save Proc.new { |r| r.history << [:before_save, :string] }, unless: "no"
- before_save Proc.new { |r| r.history << "b00m" }, unless: "yes"
- end
# Combined if and unless
before_save Proc.new { |r| r.history << [:before_save, :combined_symbol] }, if: :yes, unless: :no
before_save Proc.new { |r| r.history << "b00m" }, if: :yes, unless: :yes
@@ -489,10 +482,9 @@ module CallbacksTest
"block in run_callbacks",
"tweedle_dum",
"block in run_callbacks",
- ("call" if RUBY_VERSION < "2.3"),
"run_callbacks",
"save"
- ].compact, call_stack.map(&:label)
+ ], call_stack.map(&:label)
end
def test_short_call_stack
@@ -592,8 +584,6 @@ module CallbacksTest
[:before_save, :proc],
[:before_save, :symbol],
[:before_save, :symbol],
- [:before_save, :string],
- [:before_save, :string],
[:before_save, :combined_symbol],
], person.history
end
@@ -839,7 +829,7 @@ module CallbacksTest
def test_block_never_called_if_terminated
obj = CallbackTerminator.new
obj.save
- assert !obj.saved
+ assert_not obj.saved
end
end
@@ -867,7 +857,7 @@ module CallbacksTest
def test_block_never_called_if_abort_is_thrown
obj = CallbackDefaultTerminator.new
obj.save
- assert !obj.saved
+ assert_not obj.saved
end
end
@@ -1182,14 +1172,15 @@ module CallbacksTest
end
end
- class DeprecatedWarningTest < ActiveSupport::TestCase
- def test_deprecate_string_conditional_options
+ class NotSupportedStringConditionalTest < ActiveSupport::TestCase
+ def test_string_conditional_options
klass = Class.new(Record)
- assert_deprecated { klass.before_save :tweedle, if: "true" }
- assert_deprecated { klass.after_save :tweedle, unless: "false" }
- assert_deprecated { klass.skip_callback :save, :before, :tweedle, if: "true" }
- assert_deprecated { klass.skip_callback :save, :after, :tweedle, unless: "false" }
+ assert_raises(ArgumentError) { klass.before_save :tweedle, if: ["true"] }
+ assert_raises(ArgumentError) { klass.before_save :tweedle, if: "true" }
+ assert_raises(ArgumentError) { klass.after_save :tweedle, unless: "false" }
+ assert_raises(ArgumentError) { klass.skip_callback :save, :before, :tweedle, if: "true" }
+ assert_raises(ArgumentError) { klass.skip_callback :save, :after, :tweedle, unless: "false" }
end
end
diff --git a/activesupport/test/class_cache_test.rb b/activesupport/test/class_cache_test.rb
index 7b97028e8c..1ef1939b4b 100644
--- a/activesupport/test/class_cache_test.rb
+++ b/activesupport/test/class_cache_test.rb
@@ -11,17 +11,17 @@ module ActiveSupport
end
def test_empty?
- assert @cache.empty?
+ assert_empty @cache
@cache.store(ClassCacheTest)
- assert !@cache.empty?
+ assert_not_empty @cache
end
def test_clear!
- assert @cache.empty?
+ assert_empty @cache
@cache.store(ClassCacheTest)
- assert !@cache.empty?
+ assert_not_empty @cache
@cache.clear!
- assert @cache.empty?
+ assert_empty @cache
end
def test_set_key
@@ -40,35 +40,35 @@ module ActiveSupport
end
def test_get_constantizes
- assert @cache.empty?
+ assert_empty @cache
assert_equal ClassCacheTest, @cache.get(ClassCacheTest.name)
end
def test_get_constantizes_fails_on_invalid_names
- assert @cache.empty?
+ assert_empty @cache
assert_raise NameError do
@cache.get("OmgTotallyInvalidConstantName")
end
end
def test_get_alias
- assert @cache.empty?
+ assert_empty @cache
assert_equal @cache[ClassCacheTest.name], @cache.get(ClassCacheTest.name)
end
def test_safe_get_constantizes
- assert @cache.empty?
+ assert_empty @cache
assert_equal ClassCacheTest, @cache.safe_get(ClassCacheTest.name)
end
def test_safe_get_constantizes_doesnt_fail_on_invalid_names
- assert @cache.empty?
+ assert_empty @cache
assert_nil @cache.safe_get("OmgTotallyInvalidConstantName")
end
def test_new_rejects_strings
@cache.store ClassCacheTest.name
- assert !@cache.key?(ClassCacheTest.name)
+ assert_not @cache.key?(ClassCacheTest.name)
end
def test_store_returns_self
diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb
index ef75a320d1..98d8f3ee0d 100644
--- a/activesupport/test/concern_test.rb
+++ b/activesupport/test/concern_test.rb
@@ -91,7 +91,7 @@ class ConcernTest < ActiveSupport::TestCase
end
end
@klass.include test_module
- assert_equal false, Object.respond_to?(:test)
+ assert_not_respond_to Object, :test
Qux.class_eval do
remove_const :ClassMethods
end
diff --git a/activesupport/test/concurrency/load_interlock_aware_monitor_test.rb b/activesupport/test/concurrency/load_interlock_aware_monitor_test.rb
new file mode 100644
index 0000000000..2d0f45ec5f
--- /dev/null
+++ b/activesupport/test/concurrency/load_interlock_aware_monitor_test.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "concurrent/atomic/count_down_latch"
+require "active_support/concurrency/load_interlock_aware_monitor"
+
+module ActiveSupport
+ module Concurrency
+ class LoadInterlockAwareMonitorTest < ActiveSupport::TestCase
+ def setup
+ @monitor = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
+ end
+
+ def test_entering_with_no_blocking
+ assert @monitor.mon_enter
+ end
+
+ def test_entering_with_blocking
+ load_interlock_latch = Concurrent::CountDownLatch.new
+ monitor_latch = Concurrent::CountDownLatch.new
+
+ able_to_use_monitor = false
+ able_to_load = false
+
+ thread_with_load_interlock = Thread.new do
+ ActiveSupport::Dependencies.interlock.running do
+ load_interlock_latch.count_down
+ monitor_latch.wait
+
+ @monitor.synchronize do
+ able_to_use_monitor = true
+ end
+ end
+ end
+
+ thread_with_monitor_lock = Thread.new do
+ @monitor.synchronize do
+ monitor_latch.count_down
+ load_interlock_latch.wait
+
+ ActiveSupport::Dependencies.interlock.loading do
+ able_to_load = true
+ end
+ end
+ end
+
+ thread_with_load_interlock.join
+ thread_with_monitor_lock.join
+
+ assert able_to_use_monitor
+ assert able_to_load
+ end
+ end
+ end
+end
diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb
index 10719596df..1cf40261dc 100644
--- a/activesupport/test/configurable_test.rb
+++ b/activesupport/test/configurable_test.rb
@@ -43,11 +43,11 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase
test "configuration accessors are not available on instance" do
instance = Parent.new
- assert !instance.respond_to?(:bar)
- assert !instance.respond_to?(:bar=)
+ assert_not_respond_to instance, :bar
+ assert_not_respond_to instance, :bar=
- assert !instance.respond_to?(:baz)
- assert !instance.respond_to?(:baz=)
+ assert_not_respond_to instance, :baz
+ assert_not_respond_to instance, :baz=
end
test "configuration accessors can take a default value" do
diff --git a/activesupport/test/core_ext/array/conversions_test.rb b/activesupport/test/core_ext/array/conversions_test.rb
index 0f919efcb0..0a7c43d421 100644
--- a/activesupport/test/core_ext/array/conversions_test.rb
+++ b/activesupport/test/core_ext/array/conversions_test.rb
@@ -90,7 +90,7 @@ class ToXmlTest < ActiveSupport::TestCase
def test_to_xml_with_hash_elements
xml = [
{ name: "David", age: 26, age_in_millis: 820497600000 },
- { name: "Jason", age: 31, age_in_millis: BigDecimal.new("1.0") }
+ { name: "Jason", age: 31, age_in_millis: BigDecimal("1.0") }
].to_xml(skip_instruct: true, indent: 0)
assert_equal '<objects type="array"><object>', xml.first(30)
@@ -173,7 +173,7 @@ class ToXmlTest < ActiveSupport::TestCase
def test_to_xml_with_instruct
xml = [
{ name: "David", age: 26, age_in_millis: 820497600000 },
- { name: "Jason", age: 31, age_in_millis: BigDecimal.new("1.0") }
+ { name: "Jason", age: 31, age_in_millis: BigDecimal("1.0") }
].to_xml(skip_instruct: false, indent: 0)
assert_match(/^<\?xml [^>]*/, xml)
@@ -183,7 +183,7 @@ class ToXmlTest < ActiveSupport::TestCase
def test_to_xml_with_block
xml = [
{ name: "David", age: 26, age_in_millis: 820497600000 },
- { name: "Jason", age: 31, age_in_millis: BigDecimal.new("1.0") }
+ { name: "Jason", age: 31, age_in_millis: BigDecimal("1.0") }
].to_xml(skip_instruct: true, indent: 0) do |builder|
builder.count 2
end
diff --git a/activesupport/test/core_ext/array/grouping_test.rb b/activesupport/test/core_ext/array/grouping_test.rb
index da9d4963d8..37111a5d7d 100644
--- a/activesupport/test/core_ext/array/grouping_test.rb
+++ b/activesupport/test/core_ext/array/grouping_test.rb
@@ -4,15 +4,6 @@ require "abstract_unit"
require "active_support/core_ext/array"
class GroupingTest < ActiveSupport::TestCase
- def setup
- # In Ruby < 2.4, test we avoid Integer#/ (redefined by mathn)
- Fixnum.send :private, :/ unless 0.class == Integer
- end
-
- def teardown
- Fixnum.send :public, :/ unless 0.class == Integer
- end
-
def test_in_groups_of_with_perfect_fit
groups = []
("a".."i").to_a.in_groups_of(3) do |group|
@@ -116,7 +107,7 @@ class SplitTest < ActiveSupport::TestCase
def test_split_with_block
a = (1..10).to_a
assert_equal [[1, 2], [4, 5], [7, 8], [10]], a.split { |i| i % 3 == 0 }
- assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9 , 10], a
+ assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], a
end
def test_split_with_edge_values
diff --git a/activesupport/test/core_ext/bigdecimal_test.rb b/activesupport/test/core_ext/bigdecimal_test.rb
index 66e81f1162..62588be33b 100644
--- a/activesupport/test/core_ext/bigdecimal_test.rb
+++ b/activesupport/test/core_ext/bigdecimal_test.rb
@@ -5,7 +5,7 @@ require "active_support/core_ext/big_decimal"
class BigDecimalTest < ActiveSupport::TestCase
def test_to_s
- bd = BigDecimal.new "0.01"
+ bd = BigDecimal "0.01"
assert_equal "0.01", bd.to_s
assert_equal "+0.01", bd.to_s("+F")
assert_equal "+0.0 1", bd.to_s("+1F")
diff --git a/activesupport/test/core_ext/class_test.rb b/activesupport/test/core_ext/class_test.rb
index 9cc006fc63..5ea288738e 100644
--- a/activesupport/test/core_ext/class_test.rb
+++ b/activesupport/test/core_ext/class_test.rb
@@ -30,11 +30,11 @@ class ClassTest < ActiveSupport::TestCase
def test_descendants_excludes_singleton_classes
klass = Parent.new.singleton_class
- refute Parent.descendants.include?(klass), "descendants should not include singleton classes"
+ assert_not Parent.descendants.include?(klass), "descendants should not include singleton classes"
end
def test_subclasses_excludes_singleton_classes
klass = Parent.new.singleton_class
- refute Parent.subclasses.include?(klass), "subclasses should not include singleton classes"
+ assert_not Parent.subclasses.include?(klass), "subclasses should not include singleton classes"
end
end
diff --git a/activesupport/test/core_ext/date_and_time_behavior.rb b/activesupport/test/core_ext/date_and_time_behavior.rb
index 256353c309..b77ea22701 100644
--- a/activesupport/test/core_ext/date_and_time_behavior.rb
+++ b/activesupport/test/core_ext/date_and_time_behavior.rb
@@ -9,6 +9,11 @@ module DateAndTimeBehavior
end
def test_prev_day
+ assert_equal date_time_init(2005, 2, 24, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_day(-2)
+ assert_equal date_time_init(2005, 2, 23, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_day(-1)
+ assert_equal date_time_init(2005, 2, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_day(0)
+ assert_equal date_time_init(2005, 2, 21, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_day(1)
+ assert_equal date_time_init(2005, 2, 20, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_day(2)
assert_equal date_time_init(2005, 2, 21, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_day
assert_equal date_time_init(2005, 2, 28, 10, 10, 10), date_time_init(2005, 3, 2, 10, 10, 10).prev_day.prev_day
end
@@ -19,6 +24,11 @@ module DateAndTimeBehavior
end
def test_next_day
+ assert_equal date_time_init(2005, 2, 20, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_day(-2)
+ assert_equal date_time_init(2005, 2, 21, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_day(-1)
+ assert_equal date_time_init(2005, 2, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_day(0)
+ assert_equal date_time_init(2005, 2, 23, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_day(1)
+ assert_equal date_time_init(2005, 2, 24, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_day(2)
assert_equal date_time_init(2005, 2, 23, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_day
assert_equal date_time_init(2005, 3, 2, 10, 10, 10), date_time_init(2005, 2, 28, 10, 10, 10).next_day.next_day
end
@@ -151,6 +161,16 @@ module DateAndTimeBehavior
assert_equal date_time_init(2015, 1, 5, 15, 15, 10), date_time_init(2015, 1, 3, 15, 15, 10).next_weekday
end
+ def test_next_month
+ assert_equal date_time_init(2004, 12, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month(-2)
+ assert_equal date_time_init(2005, 1, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month(-1)
+ assert_equal date_time_init(2005, 2, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month(0)
+ assert_equal date_time_init(2005, 3, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month(1)
+ assert_equal date_time_init(2005, 4, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month(2)
+ assert_equal date_time_init(2005, 3, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month
+ assert_equal date_time_init(2005, 4, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month.next_month
+ end
+
def test_next_month_on_31st
assert_equal date_time_init(2005, 9, 30, 15, 15, 10), date_time_init(2005, 8, 31, 15, 15, 10).next_month
end
@@ -160,7 +180,13 @@ module DateAndTimeBehavior
end
def test_next_year
+ assert_equal date_time_init(2003, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year(-2)
+ assert_equal date_time_init(2004, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year(-1)
+ assert_equal date_time_init(2005, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year(0)
+ assert_equal date_time_init(2006, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year(1)
+ assert_equal date_time_init(2007, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year(2)
assert_equal date_time_init(2006, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year
+ assert_equal date_time_init(2007, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year.next_year
end
def test_prev_week
@@ -203,6 +229,16 @@ module DateAndTimeBehavior
assert_equal date_time_init(2015, 1, 2, 15, 15, 10), date_time_init(2015, 1, 4, 15, 15, 10).prev_weekday
end
+ def test_prev_month
+ assert_equal date_time_init(2005, 4, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month(-2)
+ assert_equal date_time_init(2005, 3, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month(-1)
+ assert_equal date_time_init(2005, 2, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month(0)
+ assert_equal date_time_init(2005, 1, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month(1)
+ assert_equal date_time_init(2004, 12, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month(2)
+ assert_equal date_time_init(2005, 1, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month
+ assert_equal date_time_init(2004, 12, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month.prev_month
+ end
+
def test_prev_month_on_31st
assert_equal date_time_init(2004, 2, 29, 10, 10, 10), date_time_init(2004, 3, 31, 10, 10, 10).prev_month
end
@@ -212,7 +248,21 @@ module DateAndTimeBehavior
end
def test_prev_year
+ assert_equal date_time_init(2007, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year(-2)
+ assert_equal date_time_init(2006, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year(-1)
+ assert_equal date_time_init(2005, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year(0)
+ assert_equal date_time_init(2004, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year(1)
+ assert_equal date_time_init(2003, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year(2)
assert_equal date_time_init(2004, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year
+ assert_equal date_time_init(2003, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year.prev_year
+ end
+
+ def test_last_month_on_31st
+ assert_equal date_time_init(2004, 2, 29, 0, 0, 0), date_time_init(2004, 3, 31, 0, 0, 0).last_month
+ end
+
+ def test_last_year
+ assert_equal date_time_init(2004, 6, 5, 10, 0, 0), date_time_init(2005, 6, 5, 10, 0, 0).last_year
end
def test_days_to_week_start
@@ -247,24 +297,24 @@ module DateAndTimeBehavior
def test_beginning_of_week
assert_equal date_time_init(2005, 1, 31, 0, 0, 0), date_time_init(2005, 2, 4, 10, 10, 10).beginning_of_week
- assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 28, 0, 0, 0).beginning_of_week #monday
- assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 29, 0, 0, 0).beginning_of_week #tuesday
- assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 30, 0, 0, 0).beginning_of_week #wednesday
- assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 01, 0, 0, 0).beginning_of_week #thursday
- assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 02, 0, 0, 0).beginning_of_week #friday
- assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 03, 0, 0, 0).beginning_of_week #saturday
- assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 04, 0, 0, 0).beginning_of_week #sunday
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 28, 0, 0, 0).beginning_of_week # monday
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 29, 0, 0, 0).beginning_of_week # tuesday
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 30, 0, 0, 0).beginning_of_week # wednesday
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 01, 0, 0, 0).beginning_of_week # thursday
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 02, 0, 0, 0).beginning_of_week # friday
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 03, 0, 0, 0).beginning_of_week # saturday
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 04, 0, 0, 0).beginning_of_week # sunday
end
def test_end_of_week
assert_equal date_time_init(2008, 1, 6, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 12, 31, 10, 10, 10).end_of_week
- assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 27, 0, 0, 0).end_of_week #monday
- assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 28, 0, 0, 0).end_of_week #tuesday
- assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 29, 0, 0, 0).end_of_week #wednesday
- assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 30, 0, 0, 0).end_of_week #thursday
- assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 31, 0, 0, 0).end_of_week #friday
- assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 9, 01, 0, 0, 0).end_of_week #saturday
- assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 9, 02, 0, 0, 0).end_of_week #sunday
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 27, 0, 0, 0).end_of_week # monday
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 28, 0, 0, 0).end_of_week # tuesday
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 29, 0, 0, 0).end_of_week # wednesday
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 30, 0, 0, 0).end_of_week # thursday
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 31, 0, 0, 0).end_of_week # friday
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 9, 01, 0, 0, 0).end_of_week # saturday
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 9, 02, 0, 0, 0).end_of_week # sunday
end
def test_end_of_month
@@ -278,6 +328,26 @@ module DateAndTimeBehavior
assert_equal date_time_init(2007, 12, 31, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 12, 31, 10, 10, 10).end_of_year
end
+ def test_next_occurring
+ assert_equal date_time_init(2017, 12, 18, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:monday)
+ assert_equal date_time_init(2017, 12, 19, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:tuesday)
+ assert_equal date_time_init(2017, 12, 20, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:wednesday)
+ assert_equal date_time_init(2017, 12, 21, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:thursday)
+ assert_equal date_time_init(2017, 12, 15, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:friday)
+ assert_equal date_time_init(2017, 12, 16, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:saturday)
+ assert_equal date_time_init(2017, 12, 17, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:sunday)
+ end
+
+ def test_prev_occurring
+ assert_equal date_time_init(2017, 12, 11, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:monday)
+ assert_equal date_time_init(2017, 12, 12, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:tuesday)
+ assert_equal date_time_init(2017, 12, 13, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:wednesday)
+ assert_equal date_time_init(2017, 12, 7, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:thursday)
+ assert_equal date_time_init(2017, 12, 8, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:friday)
+ assert_equal date_time_init(2017, 12, 9, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:saturday)
+ assert_equal date_time_init(2017, 12, 10, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:sunday)
+ end
+
def test_monday_with_default_beginning_of_week_set
with_bw_default(:saturday) do
assert_equal date_time_init(2012, 9, 17, 0, 0, 0), date_time_init(2012, 9, 18, 0, 0, 0).monday
@@ -291,28 +361,40 @@ module DateAndTimeBehavior
end
def test_on_weekend_on_saturday
- assert date_time_init(2015, 1, 3, 0, 0, 0).on_weekend?
- assert date_time_init(2015, 1, 3, 15, 15, 10).on_weekend?
+ assert_predicate date_time_init(2015, 1, 3, 0, 0, 0), :on_weekend?
+ assert_predicate date_time_init(2015, 1, 3, 15, 15, 10), :on_weekend?
end
def test_on_weekend_on_sunday
- assert date_time_init(2015, 1, 4, 0, 0, 0).on_weekend?
- assert date_time_init(2015, 1, 4, 15, 15, 10).on_weekend?
+ assert_predicate date_time_init(2015, 1, 4, 0, 0, 0), :on_weekend?
+ assert_predicate date_time_init(2015, 1, 4, 15, 15, 10), :on_weekend?
end
def test_on_weekend_on_monday
- assert_not date_time_init(2015, 1, 5, 0, 0, 0).on_weekend?
- assert_not date_time_init(2015, 1, 5, 15, 15, 10).on_weekend?
+ assert_not_predicate date_time_init(2015, 1, 5, 0, 0, 0), :on_weekend?
+ assert_not_predicate 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?
+ assert_not_predicate date_time_init(2015, 1, 4, 0, 0, 0), :on_weekday?
+ assert_not_predicate 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?
+ assert_predicate date_time_init(2015, 1, 5, 0, 0, 0), :on_weekday?
+ assert_predicate date_time_init(2015, 1, 5, 15, 15, 10), :on_weekday?
+ end
+
+ def test_before
+ assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 5, 12, 0, 0))
+ assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 6, 12, 0, 0))
+ assert_equal true, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 7, 12, 0, 0))
+ end
+
+ def test_after
+ assert_equal true, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 5, 12, 0, 0))
+ assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 6, 12, 0, 0))
+ assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 7, 12, 0, 0))
end
def with_bw_default(bw = :monday)
diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb
index b8672eac4b..b8652884ce 100644
--- a/activesupport/test/core_ext/date_ext_test.rb
+++ b/activesupport/test/core_ext/date_ext_test.rb
@@ -95,11 +95,11 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
def test_beginning_of_week_in_calendar_reform
- assert_equal Date.new(1582, 10, 1), Date.new(1582, 10, 15).beginning_of_week #friday
+ assert_equal Date.new(1582, 10, 1), Date.new(1582, 10, 15).beginning_of_week # friday
end
def test_end_of_week_in_calendar_reform
- assert_equal Date.new(1582, 10, 17), Date.new(1582, 10, 4).end_of_week #thursday
+ assert_equal Date.new(1582, 10, 17), Date.new(1582, 10, 4).end_of_week # thursday
end
def test_end_of_year
@@ -120,10 +120,6 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Date.new(1582, 10, 4), Date.new(1583, 10, 14).prev_year
end
- def test_last_year
- assert_equal Date.new(2004, 6, 5), Date.new(2005, 6, 5).last_year
- end
-
def test_last_year_in_leap_years
assert_equal Date.new(1999, 2, 28), Date.new(2000, 2, 29).last_year
end
@@ -148,7 +144,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Date.new(2012, 9, 28), Date.new(2005, 2, 28).advance(years: 7, months: 7)
assert_equal Date.new(2013, 10, 3), Date.new(2005, 2, 28).advance(years: 7, months: 19, days: 5)
assert_equal Date.new(2013, 10, 17), Date.new(2005, 2, 28).advance(years: 7, months: 19, weeks: 2, days: 5)
- assert_equal Date.new(2005, 2, 28), Date.new(2004, 2, 29).advance(years: 1) #leap day plus one year
+ assert_equal Date.new(2005, 2, 28), Date.new(2004, 2, 29).advance(years: 1) # leap day plus one year
end
def test_advance_does_first_years_and_then_days
@@ -185,10 +181,6 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Date.new(1582, 10, 18), Date.new(1582, 10, 4).next_week
end
- def test_last_month_on_31st
- assert_equal Date.new(2004, 2, 29), Date.new(2004, 3, 31).last_month
- end
-
def test_last_quarter_on_31st
assert_equal Date.new(2004, 2, 29), Date.new(2004, 5, 31).last_quarter
end
@@ -394,11 +386,11 @@ end
class DateExtBehaviorTest < ActiveSupport::TestCase
def test_date_acts_like_date
- assert Date.new.acts_like_date?
+ assert_predicate Date.new, :acts_like_date?
end
def test_blank?
- assert_not Date.new.blank?
+ assert_not_predicate Date.new, :blank?
end
def test_freeze_doesnt_clobber_memoized_instance_methods
diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb
index a3c2018a31..894fb80cba 100644
--- a/activesupport/test/core_ext/date_time_ext_test.rb
+++ b/activesupport/test/core_ext/date_time_ext_test.rb
@@ -30,28 +30,6 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
end
- def test_next_occur
- datetime = DateTime.new(2016, 9, 24, 0, 0) # saturday
- assert_equal datetime.next_occurring(:monday), datetime.since(2.days)
- assert_equal datetime.next_occurring(:tuesday), datetime.since(3.days)
- assert_equal datetime.next_occurring(:wednesday), datetime.since(4.days)
- assert_equal datetime.next_occurring(:thursday), datetime.since(5.days)
- assert_equal datetime.next_occurring(:friday), datetime.since(6.days)
- assert_equal datetime.next_occurring(:saturday), datetime.since(1.week)
- assert_equal datetime.next_occurring(:sunday), datetime.since(1.day)
- end
-
- def test_prev_occur
- datetime = DateTime.new(2016, 9, 24, 0, 0) # saturday
- assert_equal datetime.prev_occurring(:monday), datetime.ago(5.days)
- assert_equal datetime.prev_occurring(:tuesday), datetime.ago(4.days)
- assert_equal datetime.prev_occurring(:wednesday), datetime.ago(3.days)
- assert_equal datetime.prev_occurring(:thursday), datetime.ago(2.days)
- assert_equal datetime.prev_occurring(:friday), datetime.ago(1.day)
- assert_equal datetime.prev_occurring(:saturday), datetime.ago(1.week)
- assert_equal datetime.prev_occurring(:sunday), datetime.ago(6.days)
- end
-
def test_readable_inspect
datetime = DateTime.new(2005, 2, 21, 14, 30, 0)
assert_equal "Mon, 21 Feb 2005 14:30:00 +0000", datetime.readable_inspect
@@ -162,10 +140,6 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal DateTime.civil(2005, 4, 30, 23, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 4, 20, 10, 10, 10).end_of_month
end
- def test_last_year
- assert_equal DateTime.civil(2004, 6, 5, 10), DateTime.civil(2005, 6, 5, 10, 0, 0).last_year
- end
-
def test_ago
assert_equal DateTime.civil(2005, 2, 22, 10, 10, 9), DateTime.civil(2005, 2, 22, 10, 10, 10).ago(1)
assert_equal DateTime.civil(2005, 2, 22, 9, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).ago(3600)
@@ -213,7 +187,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal DateTime.civil(2013, 10, 3, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, days: 5)
assert_equal DateTime.civil(2013, 10, 17, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5)
assert_equal DateTime.civil(2001, 12, 27, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: -3, months: -2, days: -1)
- assert_equal DateTime.civil(2005, 2, 28, 15, 15, 10), DateTime.civil(2004, 2, 29, 15, 15, 10).advance(years: 1) #leap day plus one year
+ assert_equal DateTime.civil(2005, 2, 28, 15, 15, 10), DateTime.civil(2004, 2, 29, 15, 15, 10).advance(years: 1) # leap day plus one year
assert_equal DateTime.civil(2005, 2, 28, 20, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(hours: 5)
assert_equal DateTime.civil(2005, 2, 28, 15, 22, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(minutes: 7)
assert_equal DateTime.civil(2005, 2, 28, 15, 15, 19), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(seconds: 9)
@@ -248,10 +222,6 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
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
-
def test_last_quarter_on_31st
assert_equal DateTime.civil(2004, 2, 29), DateTime.civil(2004, 5, 31).last_quarter
end
@@ -345,15 +315,15 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_acts_like_date
- assert DateTime.new.acts_like_date?
+ assert_predicate DateTime.new, :acts_like_date?
end
def test_acts_like_time
- assert DateTime.new.acts_like_time?
+ assert_predicate DateTime.new, :acts_like_time?
end
def test_blank?
- assert_not DateTime.new.blank?
+ assert_not_predicate DateTime.new, :blank?
end
def test_utc?
@@ -393,45 +363,45 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_compare_with_time
- assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59)
- assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0)
+ assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59)
+ assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0)
assert_equal(-1, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1))
end
def test_compare_with_datetime
- assert_equal 1, DateTime.civil(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59)
- assert_equal 0, DateTime.civil(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0)
+ assert_equal 1, DateTime.civil(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59)
+ assert_equal 0, DateTime.civil(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0)
assert_equal(-1, DateTime.civil(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 1))
end
def test_compare_with_time_with_zone
- assert_equal 1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"])
- assert_equal 0, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"])
+ assert_equal 1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"])
+ assert_equal 0, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"])
assert_equal(-1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone["UTC"]))
end
def test_compare_with_string
- assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59).to_s
- assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s
+ assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59).to_s
+ assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s
assert_equal(-1, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1).to_s)
assert_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) <=> 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) <=> 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(4881173, 2)
+ assert_equal 0, DateTime.civil(1970) <=> Rational(4881175, 2)
assert_equal(-1, DateTime.civil(1970) <=> Rational(4881177, 2))
end
diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb
index b3e0cd8bd0..240ae3bde0 100644
--- a/activesupport/test/core_ext/duration_test.rb
+++ b/activesupport/test/core_ext/duration_test.rb
@@ -5,6 +5,7 @@ require "active_support/inflector"
require "active_support/time"
require "active_support/json"
require "time_zone_test_helpers"
+require "yaml"
class DurationTest < ActiveSupport::TestCase
include TimeZoneTestHelpers
@@ -15,30 +16,30 @@ class DurationTest < ActiveSupport::TestCase
assert_kind_of ActiveSupport::Duration, d
assert_kind_of Numeric, d
assert_kind_of Integer, d
- assert !d.is_a?(Hash)
+ assert_not d.is_a?(Hash)
k = Class.new
class << k; undef_method :== end
- assert !d.is_a?(k)
+ assert_not d.is_a?(k)
end
def test_instance_of
- assert 1.minute.instance_of?(1.class)
+ assert 1.minute.instance_of?(Integer)
assert 2.days.instance_of?(ActiveSupport::Duration)
- assert !3.second.instance_of?(Numeric)
+ assert_not 3.second.instance_of?(Numeric)
end
def test_threequals
assert ActiveSupport::Duration === 1.day
- assert !(ActiveSupport::Duration === 1.day.to_i)
- assert !(ActiveSupport::Duration === "foo")
+ assert_not (ActiveSupport::Duration === 1.day.to_i)
+ assert_not (ActiveSupport::Duration === "foo")
end
def test_equals
assert 1.day == 1.day
assert 1.day == 1.day.to_i
assert 1.day.to_i == 1.day
- assert !(1.day == "foo")
+ assert_not (1.day == "foo")
end
def test_to_s
@@ -52,11 +53,11 @@ class DurationTest < ActiveSupport::TestCase
assert 1.minute.eql?(1.minute)
assert 1.minute.eql?(60.seconds)
assert 2.days.eql?(48.hours)
- assert !1.second.eql?(1)
- assert !1.eql?(1.second)
+ assert_not 1.second.eql?(1)
+ assert_not 1.eql?(1.second)
assert 1.minute.eql?(180.seconds - 2.minutes)
- assert !1.minute.eql?(60)
- assert !1.minute.eql?("foo")
+ assert_not 1.minute.eql?(60)
+ assert_not 1.minute.eql?("foo")
end
def test_inspect
@@ -71,6 +72,8 @@ class DurationTest < ActiveSupport::TestCase
assert_equal "7 days", 7.days.inspect
assert_equal "1 week", 1.week.inspect
assert_equal "2 weeks", 1.fortnight.inspect
+ assert_equal "0 seconds", (10 % 5.seconds).inspect
+ assert_equal "10 minutes", (10.minutes + 0.seconds).inspect
end
def test_inspect_locale
@@ -160,7 +163,7 @@ class DurationTest < ActiveSupport::TestCase
end
def test_time_plus_duration_returns_same_time_datatype
- twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Moscow"] , Time.utc(2016, 4, 28, 00, 45))
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Moscow"], Time.utc(2016, 4, 28, 00, 45))
now = Time.now.utc
%w( second minute hour day week month year ).each do |unit|
assert_equal((now + 1.send(unit)).class, Time, "Time + 1.#{unit} must be Time")
@@ -640,6 +643,12 @@ class DurationTest < ActiveSupport::TestCase
assert_equal time + d1, time + d2
end
+ def test_durations_survive_yaml_serialization
+ d1 = YAML.load(YAML.dump(10.minutes))
+ assert_equal 600, d1.to_i
+ assert_equal 660, (d1 + 60).to_i
+ end
+
private
def eastern_time_zone
if Gem.win_platform?
diff --git a/activesupport/test/core_ext/file_test.rb b/activesupport/test/core_ext/file_test.rb
index 23e3c277cc..9c97700e5d 100644
--- a/activesupport/test/core_ext/file_test.rb
+++ b/activesupport/test/core_ext/file_test.rb
@@ -8,7 +8,7 @@ class AtomicWriteTest < ActiveSupport::TestCase
contents = "Atomic Text"
File.atomic_write(file_name, Dir.pwd) do |file|
file.write(contents)
- assert !File.exist?(file_name)
+ assert_not File.exist?(file_name)
end
assert File.exist?(file_name)
assert_equal contents, File.read(file_name)
@@ -22,7 +22,7 @@ class AtomicWriteTest < ActiveSupport::TestCase
raise "something bad"
end
rescue
- assert !File.exist?(file_name)
+ assert_not File.exist?(file_name)
end
def test_atomic_write_preserves_file_permissions
@@ -50,7 +50,7 @@ class AtomicWriteTest < ActiveSupport::TestCase
contents = "Atomic Text"
File.atomic_write(file_name, Dir.pwd) do |file|
file.write(contents)
- assert !File.exist?(file_name)
+ assert_not File.exist?(file_name)
end
assert File.exist?(file_name)
assert_equal File.probe_stat_in(Dir.pwd).mode, file_mode
diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb
index d34b7fa7b9..e481b5e4a9 100644
--- a/activesupport/test/core_ext/hash/transform_values_test.rb
+++ b/activesupport/test/core_ext/hash/transform_values_test.rb
@@ -2,25 +2,16 @@
require "abstract_unit"
require "active_support/core_ext/hash/indifferent_access"
-require "active_support/core_ext/hash/transform_values"
-class TransformValuesTest < ActiveSupport::TestCase
- test "transform_values returns a new hash with the values computed from the block" do
- original = { a: "a", b: "b" }
- mapped = original.transform_values { |v| v + "!" }
-
- assert_equal({ a: "a", b: "b" }, original)
- assert_equal({ a: "a!", b: "b!" }, mapped)
- end
-
- test "transform_values! modifies the values of the original" do
- original = { a: "a", b: "b" }
- mapped = original.transform_values! { |v| v + "!" }
-
- assert_equal({ a: "a!", b: "b!" }, original)
- assert_same original, mapped
+class TransformValuesDeprecatedRequireTest < ActiveSupport::TestCase
+ test "requiring transform_values is deprecated" do
+ assert_deprecated do
+ require "active_support/core_ext/hash/transform_values"
+ end
end
+end
+class IndifferentTransformValuesTest < ActiveSupport::TestCase
test "indifferent access is still indifferent after mapping values" do
original = { a: "a", b: "b" }.with_indifferent_access
mapped = original.transform_values { |v| v + "!" }
@@ -28,50 +19,4 @@ class TransformValuesTest < ActiveSupport::TestCase
assert_equal "a!", mapped[:a]
assert_equal "a!", mapped["a"]
end
-
- # This is to be consistent with the behavior of Ruby's built in methods
- # (e.g. #select, #reject) as of 2.2
- test "default values do not persist during mapping" do
- original = Hash.new("foo")
- original[:a] = "a"
- mapped = original.transform_values { |v| v + "!" }
-
- assert_equal "a!", mapped[:a]
- assert_nil mapped[:b]
- end
-
- test "default procs do not persist after mapping" do
- original = Hash.new { "foo" }
- original[:a] = "a"
- mapped = original.transform_values { |v| v + "!" }
-
- assert_equal "a!", mapped[:a]
- assert_nil mapped[:b]
- end
-
- test "transform_values returns a sized Enumerator if no block is given" do
- original = { a: "a", b: "b" }
- enumerator = original.transform_values
- assert_equal original.size, enumerator.size
- assert_equal Enumerator, enumerator.class
- end
-
- test "transform_values! returns a sized Enumerator if no block is given" do
- original = { a: "a", b: "b" }
- enumerator = original.transform_values!
- assert_equal original.size, enumerator.size
- assert_equal Enumerator, enumerator.class
- end
-
- test "transform_values is chainable with Enumerable methods" do
- original = { a: "a", b: "b" }
- mapped = original.transform_values.with_index { |v, i| [v, i].join }
- assert_equal({ a: "a0", b: "b1" }, mapped)
- end
-
- test "transform_values! is chainable with Enumerable methods" do
- original = { a: "a", b: "b" }
- original.transform_values!.with_index { |v, i| [v, i].join }
- assert_equal({ a: "a0", b: "b1" }, original)
- end
end
diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index 746d7ad416..f4f0dd6b31 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -45,8 +45,6 @@ class HashExtTest < ActiveSupport::TestCase
assert_respond_to h, :deep_stringify_keys!
assert_respond_to h, :to_options
assert_respond_to h, :to_options!
- assert_respond_to h, :compact
- assert_respond_to h, :compact!
assert_respond_to h, :except
assert_respond_to h, :except!
end
@@ -446,7 +444,7 @@ class HashExtTest < ActiveSupport::TestCase
original.freeze
assert_nothing_raised { original.except(:a) }
- assert_raise(RuntimeError) { original.except!(:a) }
+ assert_raise(frozen_error_class) { original.except!(:a) }
end
def test_except_does_not_delete_values_in_original
@@ -456,38 +454,10 @@ class HashExtTest < ActiveSupport::TestCase
end
end
- def test_compact
- hash_contain_nil_value = @symbols.merge(z: nil)
- hash_with_only_nil_values = { a: nil, b: nil }
-
- h = hash_contain_nil_value.dup
- assert_equal(@symbols, h.compact)
- assert_equal(hash_contain_nil_value, h)
-
- h = hash_with_only_nil_values.dup
- assert_equal({}, h.compact)
- assert_equal(hash_with_only_nil_values, h)
-
- h = @symbols.dup
- assert_equal(@symbols, h.compact)
- assert_equal(@symbols, h)
- end
-
- def test_compact!
- hash_contain_nil_value = @symbols.merge(z: nil)
- hash_with_only_nil_values = { a: nil, b: nil }
-
- h = hash_contain_nil_value.dup
- assert_equal(@symbols, h.compact!)
- assert_equal(@symbols, h)
-
- h = hash_with_only_nil_values.dup
- assert_equal({}, h.compact!)
- assert_equal({}, h)
-
- h = @symbols.dup
- assert_nil(h.compact!)
- assert_equal(@symbols, h)
+ def test_requiring_compact_is_deprecated
+ assert_deprecated do
+ require "active_support/core_ext/hash/compact"
+ end
end
end
@@ -1061,7 +1031,7 @@ class HashToXmlTest < ActiveSupport::TestCase
</alert>
XML
alert_at = Hash.from_xml(alert_xml)["alert"]["alert_at"]
- assert alert_at.utc?
+ assert_predicate alert_at, :utc?
assert_equal Time.utc(2008, 2, 10, 15, 30, 45), alert_at
end
@@ -1072,7 +1042,7 @@ class HashToXmlTest < ActiveSupport::TestCase
</alert>
XML
alert_at = Hash.from_xml(alert_xml)["alert"]["alert_at"]
- assert alert_at.utc?
+ assert_predicate alert_at, :utc?
assert_equal Time.utc(2008, 2, 10, 15, 30, 45), alert_at
end
@@ -1083,7 +1053,7 @@ class HashToXmlTest < ActiveSupport::TestCase
</alert>
XML
alert_at = Hash.from_xml(alert_xml)["alert"]["alert_at"]
- assert alert_at.utc?
+ assert_predicate alert_at, :utc?
assert_equal 2050, alert_at.year
assert_equal 2, alert_at.month
assert_equal 10, alert_at.day
diff --git a/activesupport/test/core_ext/integer_ext_test.rb b/activesupport/test/core_ext/integer_ext_test.rb
index 14169b084d..5691dc5341 100644
--- a/activesupport/test/core_ext/integer_ext_test.rb
+++ b/activesupport/test/core_ext/integer_ext_test.rb
@@ -8,14 +8,14 @@ class IntegerExtTest < ActiveSupport::TestCase
def test_multiple_of
[ -7, 0, 7, 14 ].each { |i| assert i.multiple_of?(7) }
- [ -7, 7, 14 ].each { |i| assert ! i.multiple_of?(6) }
+ [ -7, 7, 14 ].each { |i| assert_not i.multiple_of?(6) }
# test the 0 edge case
assert 0.multiple_of?(0)
- assert !5.multiple_of?(0)
+ assert_not 5.multiple_of?(0)
# test with a prime
- [2, 3, 5, 7].each { |i| assert !PRIME.multiple_of?(i) }
+ [2, 3, 5, 7].each { |i| assert_not PRIME.multiple_of?(i) }
end
def test_ordinalize
diff --git a/activesupport/test/core_ext/load_error_test.rb b/activesupport/test/core_ext/load_error_test.rb
index 4fdd228ff8..41b11d0c33 100644
--- a/activesupport/test/core_ext/load_error_test.rb
+++ b/activesupport/test/core_ext/load_error_test.rb
@@ -5,7 +5,7 @@ require "active_support/core_ext/load_error"
class TestLoadError < ActiveSupport::TestCase
def test_with_require
- assert_raise(LoadError) { require 'no_this_file_don\'t_exist' }
+ assert_raise(LoadError) { require "no_this_file_don't_exist" }
end
def test_with_load
assert_raise(LoadError) { load "nor_does_this_one" }
diff --git a/activesupport/test/core_ext/module/anonymous_test.rb b/activesupport/test/core_ext/module/anonymous_test.rb
index 606f22c9b5..e03c217015 100644
--- a/activesupport/test/core_ext/module/anonymous_test.rb
+++ b/activesupport/test/core_ext/module/anonymous_test.rb
@@ -5,12 +5,12 @@ require "active_support/core_ext/module/anonymous"
class AnonymousTest < ActiveSupport::TestCase
test "an anonymous class or module are anonymous" do
- assert Module.new.anonymous?
- assert Class.new.anonymous?
+ assert_predicate Module.new, :anonymous?
+ assert_predicate Class.new, :anonymous?
end
test "a named class or module are not anonymous" do
- assert !Kernel.anonymous?
- assert !Object.anonymous?
+ assert_not_predicate Kernel, :anonymous?
+ assert_not_predicate Object, :anonymous?
end
end
diff --git a/activesupport/test/core_ext/module/attr_internal_test.rb b/activesupport/test/core_ext/module/attr_internal_test.rb
index c2a28eced4..9a65f75497 100644
--- a/activesupport/test/core_ext/module/attr_internal_test.rb
+++ b/activesupport/test/core_ext/module/attr_internal_test.rb
@@ -12,7 +12,7 @@ class AttrInternalTest < ActiveSupport::TestCase
def test_reader
assert_nothing_raised { @target.attr_internal_reader :foo }
- assert !@instance.instance_variable_defined?("@_foo")
+ assert_not @instance.instance_variable_defined?("@_foo")
assert_raise(NoMethodError) { @instance.foo = 1 }
@instance.instance_variable_set("@_foo", 1)
@@ -22,7 +22,7 @@ class AttrInternalTest < ActiveSupport::TestCase
def test_writer
assert_nothing_raised { @target.attr_internal_writer :foo }
- assert !@instance.instance_variable_defined?("@_foo")
+ assert_not @instance.instance_variable_defined?("@_foo")
assert_nothing_raised { assert_equal 1, @instance.foo = 1 }
assert_equal 1, @instance.instance_variable_get("@_foo")
@@ -32,7 +32,7 @@ class AttrInternalTest < ActiveSupport::TestCase
def test_accessor
assert_nothing_raised { @target.attr_internal :foo }
- assert !@instance.instance_variable_defined?("@_foo")
+ assert_not @instance.instance_variable_defined?("@_foo")
assert_nothing_raised { assert_equal 1, @instance.foo = 1 }
assert_equal 1, @instance.instance_variable_get("@_foo")
@@ -44,10 +44,10 @@ class AttrInternalTest < ActiveSupport::TestCase
assert_nothing_raised { Module.attr_internal_naming_format = "@abc%sdef" }
@target.attr_internal :foo
- assert !@instance.instance_variable_defined?("@_foo")
- assert !@instance.instance_variable_defined?("@abcfoodef")
+ assert_not @instance.instance_variable_defined?("@_foo")
+ assert_not @instance.instance_variable_defined?("@abcfoodef")
assert_nothing_raised { @instance.foo = 1 }
- assert !@instance.instance_variable_defined?("@_foo")
+ assert_not @instance.instance_variable_defined?("@_foo")
assert @instance.instance_variable_defined?("@abcfoodef")
ensure
Module.attr_internal_naming_format = "@_%s"
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 e0fbd1002c..e0e331fc91 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
@@ -43,22 +43,22 @@ class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase
assert_respond_to @class, :foo
assert_respond_to @class, :foo=
assert_respond_to @object, :bar
- assert !@object.respond_to?(:bar=)
+ assert_not_respond_to @object, :bar=
end.join
end
def test_should_not_create_instance_reader
Thread.new do
assert_respond_to @class, :shaq
- assert !@object.respond_to?(:shaq)
+ assert_not_respond_to @object, :shaq
end.join
end
def test_should_not_create_instance_accessors
Thread.new do
assert_respond_to @class, :camp
- assert !@object.respond_to?(:camp)
- assert !@object.respond_to?(:camp=)
+ assert_not_respond_to @object, :camp
+ assert_not_respond_to @object, :camp=
end.join
end
diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb
index f1d6859a88..33c583947a 100644
--- a/activesupport/test/core_ext/module/attribute_accessor_test.rb
+++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb
@@ -65,18 +65,18 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
assert_respond_to @module, :foo
assert_respond_to @module, :foo=
assert_respond_to @object, :bar
- assert !@object.respond_to?(:bar=)
+ assert_not_respond_to @object, :bar=
end
def test_should_not_create_instance_reader
assert_respond_to @module, :shaq
- assert !@object.respond_to?(:shaq)
+ assert_not_respond_to @object, :shaq
end
def test_should_not_create_instance_accessors
assert_respond_to @module, :camp
- assert !@object.respond_to?(:camp)
- assert !@object.respond_to?(:camp=)
+ assert_not_respond_to @object, :camp
+ assert_not_respond_to @object, :camp=
end
def test_should_raise_name_error_if_attribute_name_is_invalid
diff --git a/activesupport/test/core_ext/module/attribute_aliasing_test.rb b/activesupport/test/core_ext/module/attribute_aliasing_test.rb
index 187a0f4da2..81aac224f9 100644
--- a/activesupport/test/core_ext/module/attribute_aliasing_test.rb
+++ b/activesupport/test/core_ext/module/attribute_aliasing_test.rb
@@ -30,15 +30,15 @@ class AttributeAliasingTest < ActiveSupport::TestCase
def test_attribute_alias
e = AttributeAliasing::Email.new
- assert !e.subject?
+ assert_not_predicate e, :subject?
e.title = "Upgrade computer"
assert_equal "Upgrade computer", e.subject
- assert e.subject?
+ assert_predicate e, :subject?
e.subject = "We got a long way to go"
assert_equal "We got a long way to go", e.title
- assert e.title?
+ assert_predicate e, :title?
end
def test_aliasing_to_uppercase_attributes
@@ -47,15 +47,15 @@ class AttributeAliasingTest < ActiveSupport::TestCase
# to more sensible ones, everything goes *foof*.
e = AttributeAliasing::Email.new
- assert !e.body?
- assert !e.Data?
+ assert_not_predicate e, :body?
+ assert_not_predicate e, :Data?
e.body = "No, really, this is not a joke."
assert_equal "No, really, this is not a joke.", e.Data
- assert e.Data?
+ assert_predicate e, :Data?
e.Data = "Uppercased methods are the suck"
assert_equal "Uppercased methods are the suck", e.body
- assert e.body?
+ assert_predicate e, :body?
end
end
diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb
index 192c3d5a9c..374114c11b 100644
--- a/activesupport/test/core_ext/module/concerning_test.rb
+++ b/activesupport/test/core_ext/module/concerning_test.rb
@@ -21,7 +21,7 @@ class ModuleConcernTest < ActiveSupport::TestCase
# Declares a concern but doesn't include it
assert klass.const_defined?(:Baz, false)
- assert !ModuleConcernTest.const_defined?(:Baz)
+ assert_not ModuleConcernTest.const_defined?(:Baz)
assert_kind_of ActiveSupport::Concern, klass::Baz
assert_not_includes klass.ancestors, klass::Baz, klass.ancestors.inspect
@@ -55,10 +55,10 @@ class ModuleConcernTest < ActiveSupport::TestCase
end
def test_using_class_methods_blocks_instead_of_ClassMethods_module
- assert !Foo.respond_to?(:will_be_orphaned)
- assert Foo.respond_to?(:hacked_on)
- assert Foo.respond_to?(:nicer_dsl)
- assert Foo.respond_to?(:doesnt_clobber)
+ assert_not_respond_to Foo, :will_be_orphaned
+ assert_respond_to Foo, :hacked_on
+ assert_respond_to Foo, :nicer_dsl
+ assert_respond_to Foo, :doesnt_clobber
# Orphan in Foo::ClassMethods, not Bar::ClassMethods.
assert Foo.const_defined?(:ClassMethods)
diff --git a/activesupport/test/core_ext/module/reachable_test.rb b/activesupport/test/core_ext/module/reachable_test.rb
index a69fc6839e..f356d46957 100644
--- a/activesupport/test/core_ext/module/reachable_test.rb
+++ b/activesupport/test/core_ext/module/reachable_test.rb
@@ -5,13 +5,17 @@ require "active_support/core_ext/module/reachable"
class AnonymousTest < ActiveSupport::TestCase
test "an anonymous class or module is not reachable" do
- assert !Module.new.reachable?
- assert !Class.new.reachable?
+ assert_deprecated do
+ assert_not_predicate Module.new, :reachable?
+ assert_not_predicate Class.new, :reachable?
+ end
end
test "ordinary named classes or modules are reachable" do
- assert Kernel.reachable?
- assert Object.reachable?
+ assert_deprecated do
+ assert_predicate Kernel, :reachable?
+ assert_predicate Object, :reachable?
+ end
end
test "a named class or module whose constant has gone is not reachable" do
@@ -21,8 +25,10 @@ class AnonymousTest < ActiveSupport::TestCase
self.class.send(:remove_const, :C)
self.class.send(:remove_const, :M)
- assert !c.reachable?
- assert !m.reachable?
+ assert_deprecated do
+ assert_not_predicate c, :reachable?
+ assert_not_predicate m, :reachable?
+ end
end
test "a named class or module whose constants store different objects are not reachable" do
@@ -35,9 +41,11 @@ class AnonymousTest < ActiveSupport::TestCase
eval "class C; end"
eval "module M; end"
- assert C.reachable?
- assert M.reachable?
- assert !c.reachable?
- assert !m.reachable?
+ assert_deprecated do
+ assert_predicate C, :reachable?
+ assert_predicate M, :reachable?
+ assert_not_predicate c, :reachable?
+ assert_not_predicate m, :reachable?
+ end
end
end
diff --git a/activesupport/test/core_ext/module/remove_method_test.rb b/activesupport/test/core_ext/module/remove_method_test.rb
index dbf71b477d..a18fc0a5e4 100644
--- a/activesupport/test/core_ext/module/remove_method_test.rb
+++ b/activesupport/test/core_ext/module/remove_method_test.rb
@@ -6,22 +6,22 @@ require "active_support/core_ext/module/remove_method"
module RemoveMethodTests
class A
def do_something
- return 1
+ 1
end
def do_something_protected
- return 1
+ 1
end
protected :do_something_protected
def do_something_private
- return 1
+ 1
end
private :do_something_private
class << self
def do_something_else
- return 2
+ 2
end
end
end
@@ -32,14 +32,14 @@ class RemoveMethodTest < ActiveSupport::TestCase
RemoveMethodTests::A.class_eval {
remove_possible_method(:do_something)
}
- assert !RemoveMethodTests::A.new.respond_to?(:do_something)
+ assert_not_respond_to RemoveMethodTests::A.new, :do_something
end
def test_remove_singleton_method_from_an_object
RemoveMethodTests::A.class_eval {
remove_possible_singleton_method(:do_something_else)
}
- assert !RemoveMethodTests::A.respond_to?(:do_something_else)
+ assert_not_respond_to RemoveMethodTests::A, :do_something_else
end
def test_redefine_method_in_an_object
diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb
index e918823074..04692f1484 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -347,7 +347,7 @@ class ModuleTest < ActiveSupport::TestCase
def test_delegation_with_method_arguments
has_block = HasBlock.new(Block.new)
- assert has_block.hello?
+ assert_predicate has_block, :hello?
end
def test_delegate_missing_to_with_method
@@ -383,9 +383,9 @@ class ModuleTest < ActiveSupport::TestCase
end
def test_delegate_missing_to_affects_respond_to
- assert DecoratedTester.new(@david).respond_to?(:name)
- assert_not DecoratedTester.new(@david).respond_to?(:private_name)
- assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method)
+ assert_respond_to DecoratedTester.new(@david), :name
+ assert_not_respond_to DecoratedTester.new(@david), :private_name
+ assert_not_respond_to DecoratedTester.new(@david), :my_fake_method
assert DecoratedTester.new(@david).respond_to?(:name, true)
assert_not DecoratedTester.new(@david).respond_to?(:private_name, true)
@@ -414,8 +414,8 @@ class ModuleTest < ActiveSupport::TestCase
place = location.new(Somewhere.new("Such street", "Sad city"))
- assert_not place.respond_to?(:street)
- assert_not place.respond_to?(:city)
+ assert_not_respond_to place, :street
+ assert_not_respond_to place, :city
assert place.respond_to?(:street, true) # Asking for private method
assert place.respond_to?(:city, true)
@@ -432,12 +432,79 @@ class ModuleTest < ActiveSupport::TestCase
place = location.new(Somewhere.new("Such street", "Sad city"))
- assert_not place.respond_to?(:street)
- assert_not place.respond_to?(:city)
+ assert_not_respond_to place, :street
+ assert_not_respond_to place, :city
- assert_not place.respond_to?(:the_street)
+ assert_not_respond_to place, :the_street
assert place.respond_to?(:the_street, true)
- assert_not place.respond_to?(:the_city)
+ assert_not_respond_to place, :the_city
assert place.respond_to?(:the_city, true)
end
+
+ def test_private_delegate_with_private_option
+ location = Class.new do
+ def initialize(place)
+ @place = place
+ end
+
+ delegate(:street, :city, to: :@place, private: true)
+ end
+
+ place = location.new(Somewhere.new("Such street", "Sad city"))
+
+ assert_not_respond_to place, :street
+ assert_not_respond_to place, :city
+
+ assert place.respond_to?(:street, true) # Asking for private method
+ assert place.respond_to?(:city, true)
+ end
+
+ def test_some_public_some_private_delegate_with_private_option
+ location = Class.new do
+ def initialize(place)
+ @place = place
+ end
+
+ delegate(:street, to: :@place)
+ delegate(:city, to: :@place, private: true)
+ end
+
+ place = location.new(Somewhere.new("Such street", "Sad city"))
+
+ assert_respond_to place, :street
+ assert_not_respond_to place, :city
+
+ assert place.respond_to?(:city, true) # Asking for private method
+ end
+
+ def test_private_delegate_prefixed_with_private_option
+ location = Class.new do
+ def initialize(place)
+ @place = place
+ end
+
+ delegate(:street, :city, to: :@place, prefix: :the, private: true)
+ end
+
+ place = location.new(Somewhere.new("Such street", "Sad city"))
+
+ assert_not_respond_to place, :the_street
+ assert place.respond_to?(:the_street, true)
+ assert_not_respond_to place, :the_city
+ assert place.respond_to?(:the_city, true)
+ end
+
+ def test_delegate_with_private_option_returns_names_of_delegate_methods
+ location = Class.new do
+ def initialize(place)
+ @place = place
+ end
+ end
+
+ assert_equal [:street, :city],
+ location.delegate(:street, :city, to: :@place, private: true)
+
+ assert_equal [:the_street, :the_city],
+ location.delegate(:street, :city, to: :@place, prefix: :the, private: true)
+ end
end
diff --git a/activesupport/test/core_ext/name_error_test.rb b/activesupport/test/core_ext/name_error_test.rb
index d1dace3713..5c6c12ffc7 100644
--- a/activesupport/test/core_ext/name_error_test.rb
+++ b/activesupport/test/core_ext/name_error_test.rb
@@ -17,7 +17,7 @@ class NameErrorTest < ActiveSupport::TestCase
exc = assert_raise NameError do
some_method_that_does_not_exist
end
- assert !exc.missing_name?(:Foo)
+ assert_not exc.missing_name?(:Foo)
assert_nil exc.missing_name
end
end
diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb
index d38124b214..5005b9febd 100644
--- a/activesupport/test/core_ext/numeric_ext_test.rb
+++ b/activesupport/test/core_ext/numeric_ext_test.rb
@@ -302,7 +302,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
assert_equal "40 KB", 41100.to_s(:human_size, precision: 2)
assert_equal "1.0 KB", kilobytes(1.0123).to_s(:human_size, precision: 2, strip_insignificant_zeros: false)
assert_equal "1.012 KB", kilobytes(1.0123).to_s(:human_size, precision: 3, significant: false)
- assert_equal "1 KB", kilobytes(1.0123).to_s(:human_size, precision: 0, significant: true) #ignores significant it precision is 0
+ assert_equal "1 KB", kilobytes(1.0123).to_s(:human_size, precision: 0, significant: true) # ignores significant it precision is 0
end
def test_to_s__human_size_with_custom_delimiter_and_separator
@@ -330,17 +330,17 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
assert_equal "489.0 Thousand", 489000.to_s(:human, precision: 4, strip_insignificant_zeros: false)
assert_equal "1.2346 Million", 1234567.to_s(:human, precision: 4, significant: false)
assert_equal "1,2 Million", 1234567.to_s(:human, precision: 1, significant: false, separator: ",")
- assert_equal "1 Million", 1234567.to_s(:human, precision: 0, significant: true, separator: ",") #significant forced to false
+ assert_equal "1 Million", 1234567.to_s(:human, precision: 0, significant: true, separator: ",") # significant forced to false
end
def test_number_to_human_with_custom_units
- #Only integers
+ # Only integers
volume = { unit: "ml", thousand: "lt", million: "m3" }
assert_equal "123 lt", 123456.to_s(:human, units: volume)
assert_equal "12 ml", 12.to_s(:human, units: volume)
assert_equal "1.23 m3", 1234567.to_s(:human, units: volume)
- #Including fractionals
+ # Including fractionals
distance = { mili: "mm", centi: "cm", deci: "dm", unit: "m", ten: "dam", hundred: "hm", thousand: "km" }
assert_equal "1.23 mm", 0.00123.to_s(:human, units: distance)
assert_equal "1.23 cm", 0.0123.to_s(:human, units: distance)
@@ -353,14 +353,14 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
assert_equal "1.23 km", 1230.to_s(:human, units: distance)
assert_equal "12.3 km", 12300.to_s(:human, units: distance)
- #The quantifiers don't need to be a continuous sequence
+ # The quantifiers don't need to be a continuous sequence
gangster = { hundred: "hundred bucks", million: "thousand quids" }
assert_equal "1 hundred bucks", 100.to_s(:human, units: gangster)
assert_equal "25 hundred bucks", 2500.to_s(:human, units: gangster)
assert_equal "25 thousand quids", 25000000.to_s(:human, units: gangster)
assert_equal "12300 thousand quids", 12345000000.to_s(:human, units: gangster)
- #Spaces are stripped from the resulting string
+ # Spaces are stripped from the resulting string
assert_equal "4", 4.to_s(:human, units: { unit: "", ten: "tens " })
assert_equal "4.5 tens", 45.to_s(:human, units: { unit: "", ten: " tens " })
end
@@ -406,86 +406,9 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
assert_equal 10_000, 10.seconds.in_milliseconds
end
- # TODO: Remove positive and negative tests when we drop support to ruby < 2.3
- b = 2**64
-
- T_ZERO = b.coerce(0).first
- T_ONE = b.coerce(1).first
- T_MONE = b.coerce(-1).first
-
- def test_positive
- assert_predicate(1, :positive?)
- assert_not_predicate(0, :positive?)
- assert_not_predicate(-1, :positive?)
- assert_predicate(+1.0, :positive?)
- assert_not_predicate(+0.0, :positive?)
- assert_not_predicate(-0.0, :positive?)
- assert_not_predicate(-1.0, :positive?)
- assert_predicate(+(0.0.next_float), :positive?)
- assert_not_predicate(-(0.0.next_float), :positive?)
- assert_predicate(Float::INFINITY, :positive?)
- assert_not_predicate(-Float::INFINITY, :positive?)
- assert_not_predicate(Float::NAN, :positive?)
-
- a = Class.new(Numeric) do
- def >(x); true; end
- end.new
- assert_predicate(a, :positive?)
-
- a = Class.new(Numeric) do
- def >(x); false; end
- end.new
- assert_not_predicate(a, :positive?)
-
- assert_predicate(1 / 2r, :positive?)
- assert_not_predicate(-1 / 2r, :positive?)
-
- assert_predicate(T_ONE, :positive?)
- assert_not_predicate(T_MONE, :positive?)
- assert_not_predicate(T_ZERO, :positive?)
-
- e = assert_raises(NoMethodError) do
- Complex(1).positive?
+ def test_requiring_inquiry_is_deprecated
+ assert_deprecated do
+ require "active_support/core_ext/numeric/inquiry"
end
-
- assert_match(/positive\?/, e.message)
- end
-
- def test_negative
- assert_predicate(-1, :negative?)
- assert_not_predicate(0, :negative?)
- assert_not_predicate(1, :negative?)
- assert_predicate(-1.0, :negative?)
- assert_not_predicate(-0.0, :negative?)
- assert_not_predicate(+0.0, :negative?)
- assert_not_predicate(+1.0, :negative?)
- assert_predicate(-(0.0.next_float), :negative?)
- assert_not_predicate(+(0.0.next_float), :negative?)
- assert_predicate(-Float::INFINITY, :negative?)
- assert_not_predicate(Float::INFINITY, :negative?)
- assert_not_predicate(Float::NAN, :negative?)
-
- a = Class.new(Numeric) do
- def <(x); true; end
- end.new
- assert_predicate(a, :negative?)
-
- a = Class.new(Numeric) do
- def <(x); false; end
- end.new
- assert_not_predicate(a, :negative?)
-
- assert_predicate(-1 / 2r, :negative?)
- assert_not_predicate(1 / 2r, :negative?)
-
- assert_not_predicate(T_ONE, :negative?)
- assert_predicate(T_MONE, :negative?)
- assert_not_predicate(T_ZERO, :negative?)
-
- e = assert_raises(NoMethodError) do
- Complex(1).negative?
- end
-
- assert_match(/negative\?/, e.message)
end
end
diff --git a/activesupport/test/core_ext/object/acts_like_test.rb b/activesupport/test/core_ext/object/acts_like_test.rb
index 9f7b81f7fc..31241caf0a 100644
--- a/activesupport/test/core_ext/object/acts_like_test.rb
+++ b/activesupport/test/core_ext/object/acts_like_test.rb
@@ -17,19 +17,19 @@ class ObjectTests < ActiveSupport::TestCase
dt = DateTime.new
duck = DuckTime.new
- assert !object.acts_like?(:time)
- assert !object.acts_like?(:date)
+ assert_not object.acts_like?(:time)
+ assert_not object.acts_like?(:date)
assert time.acts_like?(:time)
- assert !time.acts_like?(:date)
+ assert_not time.acts_like?(:date)
- assert !date.acts_like?(:time)
+ assert_not date.acts_like?(:time)
assert date.acts_like?(:date)
assert dt.acts_like?(:time)
assert dt.acts_like?(:date)
assert duck.acts_like?(:time)
- assert !duck.acts_like?(:date)
+ assert_not duck.acts_like?(:date)
end
end
diff --git a/activesupport/test/core_ext/object/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb
index 749e59ec00..954f415383 100644
--- a/activesupport/test/core_ext/object/blank_test.rb
+++ b/activesupport/test/core_ext/object/blank_test.rb
@@ -16,8 +16,8 @@ class BlankTest < ActiveSupport::TestCase
end
end
- BLANK = [ EmptyTrue.new, nil, false, "", " ", " \n\t \r ", " ", "\u00a0", [], {} ]
- NOT = [ EmptyFalse.new, Object.new, true, 0, 1, "a", [nil], { nil => 0 }, Time.now ]
+ BLANK = [ EmptyTrue.new, nil, false, "", " ", " \n\t \r ", " ", "\u00a0", [], {}, " ".encode("UTF-16LE") ]
+ NOT = [ EmptyFalse.new, Object.new, true, 0, 1, "a", [nil], { nil => 0 }, Time.now, "my value".encode("UTF-16LE") ]
def test_blank
BLANK.each { |v| assert_equal true, v.blank?, "#{v.inspect} should be blank" }
diff --git a/activesupport/test/core_ext/object/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb
index 2486592441..1fb26ebac7 100644
--- a/activesupport/test/core_ext/object/deep_dup_test.rb
+++ b/activesupport/test/core_ext/object/deep_dup_test.rb
@@ -47,7 +47,7 @@ class DeepDupTest < ActiveSupport::TestCase
object = Object.new
dup = object.deep_dup
dup.instance_variable_set(:@a, 1)
- assert !object.instance_variable_defined?(:@a)
+ assert_not object.instance_variable_defined?(:@a)
assert dup.instance_variable_defined?(:@a)
end
diff --git a/activesupport/test/core_ext/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb
index 93f39b00bb..635dd7f281 100644
--- a/activesupport/test/core_ext/object/duplicable_test.rb
+++ b/activesupport/test/core_ext/object/duplicable_test.rb
@@ -8,16 +8,10 @@ require "active_support/core_ext/numeric/time"
class DuplicableTest < ActiveSupport::TestCase
if RUBY_VERSION >= "2.5.0"
RAISE_DUP = [method(:puts)]
- ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal.new("4.56"), nil, false, true, 1, 2.3, Complex(1), Rational(1)]
- elsif RUBY_VERSION >= "2.4.1"
- RAISE_DUP = [method(:puts), Complex(1), Rational(1)]
- ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal.new("4.56"), nil, false, true, 1, 2.3]
- elsif RUBY_VERSION >= "2.4.0" # Due to 2.4.0 bug. This elsif cannot be removed unless we drop 2.4.0 support...
- RAISE_DUP = [method(:puts), Complex(1), Rational(1), "symbol_from_string".to_sym]
- ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal.new("4.56"), nil, false, true, 1, 2.3]
+ ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3, Complex(1), Rational(1)]
else
- RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, method(:puts), Complex(1), Rational(1)]
- ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal.new("4.56")]
+ RAISE_DUP = [method(:puts), Complex(1), Rational(1)]
+ ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3]
end
def test_duplicable
diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb
index 52c21f2e8e..8cbb4f848f 100644
--- a/activesupport/test/core_ext/object/inclusion_test.rb
+++ b/activesupport/test/core_ext/object/inclusion_test.rb
@@ -6,30 +6,30 @@ require "active_support/core_ext/object/inclusion"
class InTest < ActiveSupport::TestCase
def test_in_array
assert 1.in?([1, 2])
- assert !3.in?([1, 2])
+ assert_not 3.in?([1, 2])
end
def test_in_hash
h = { "a" => 100, "b" => 200 }
assert "a".in?(h)
- assert !"z".in?(h)
+ assert_not "z".in?(h)
end
def test_in_string
assert "lo".in?("hello")
- assert !"ol".in?("hello")
+ assert_not "ol".in?("hello")
assert ?h.in?("hello")
end
def test_in_range
assert 25.in?(1..50)
- assert !75.in?(1..50)
+ assert_not 75.in?(1..50)
end
def test_in_set
s = Set.new([1, 2])
assert 1.in?(s)
- assert !3.in?(s)
+ assert_not 3.in?(s)
end
module A
@@ -45,8 +45,8 @@ class InTest < ActiveSupport::TestCase
def test_in_module
assert A.in?(B)
assert A.in?(C)
- assert !A.in?(A)
- assert !A.in?(D)
+ assert_not A.in?(A)
+ assert_not A.in?(D)
end
def test_no_method_catching
diff --git a/activesupport/test/core_ext/object/instance_variables_test.rb b/activesupport/test/core_ext/object/instance_variables_test.rb
index b9ec827954..a3d8daab5b 100644
--- a/activesupport/test/core_ext/object/instance_variables_test.rb
+++ b/activesupport/test/core_ext/object/instance_variables_test.rb
@@ -19,7 +19,7 @@ class ObjectInstanceVariableTest < ActiveSupport::TestCase
end
def test_instance_exec_passes_arguments_to_block
- assert_equal %w(hello goodbye), "hello".instance_exec("goodbye") { |v| [self, v] }
+ assert_equal %w(hello goodbye), "hello".dup.instance_exec("goodbye") { |v| [self, v] }
end
def test_instance_exec_with_frozen_obj
@@ -27,7 +27,7 @@ class ObjectInstanceVariableTest < ActiveSupport::TestCase
end
def test_instance_exec_nested
- assert_equal %w(goodbye olleh bar), "hello".instance_exec("goodbye") { |arg|
+ assert_equal %w(goodbye olleh bar), "hello".dup.instance_exec("goodbye") { |arg|
[arg] + instance_exec("bar") { |v| [reverse, v] } }
end
end
diff --git a/activesupport/test/core_ext/object/try_test.rb b/activesupport/test/core_ext/object/try_test.rb
index fe68b24bf5..a838334034 100644
--- a/activesupport/test/core_ext/object/try_test.rb
+++ b/activesupport/test/core_ext/object/try_test.rb
@@ -10,25 +10,25 @@ class ObjectTryTest < ActiveSupport::TestCase
def test_nonexisting_method
method = :undefined_method
- assert !@string.respond_to?(method)
+ assert_not_respond_to @string, method
assert_nil @string.try(method)
end
def test_nonexisting_method_with_arguments
method = :undefined_method
- assert !@string.respond_to?(method)
+ assert_not_respond_to @string, method
assert_nil @string.try(method, "llo", "y")
end
def test_nonexisting_method_bang
method = :undefined_method
- assert !@string.respond_to?(method)
+ assert_not_respond_to @string, method
assert_raise(NoMethodError) { @string.try!(method) }
end
def test_nonexisting_method_with_arguments_bang
method = :undefined_method
- assert !@string.respond_to?(method)
+ assert_not_respond_to @string, method
assert_raise(NoMethodError) { @string.try!(method, "llo", "y") }
end
@@ -78,7 +78,6 @@ class ObjectTryTest < ActiveSupport::TestCase
def test_try_with_private_method_bang
klass = Class.new do
private
-
def private_method
"private method"
end
@@ -90,7 +89,6 @@ class ObjectTryTest < ActiveSupport::TestCase
def test_try_with_private_method
klass = Class.new do
private
-
def private_method
"private method"
end
@@ -109,7 +107,6 @@ class ObjectTryTest < ActiveSupport::TestCase
end
private
-
def private_delegator_method
"private delegator method"
end
@@ -120,11 +117,11 @@ class ObjectTryTest < ActiveSupport::TestCase
end
def test_try_with_method_on_delegator_target
- assert_equal 5, Decorator.new(@string).size
+ assert_equal 5, Decorator.new(@string).try(:size)
end
def test_try_with_overridden_method_on_delegator
- assert_equal "overridden reverse", Decorator.new(@string).reverse
+ assert_equal "overridden reverse", Decorator.new(@string).try(:reverse)
end
def test_try_with_private_method_on_delegator
@@ -140,7 +137,6 @@ class ObjectTryTest < ActiveSupport::TestCase
def test_try_with_private_method_on_delegator_target
klass = Class.new do
private
-
def private_method
"private method"
end
@@ -152,7 +148,6 @@ class ObjectTryTest < ActiveSupport::TestCase
def test_try_with_private_method_on_delegator_target_bang
klass = Class.new do
private
-
def private_method
"private method"
end
diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb
index a96e3d62e8..7c7a78f461 100644
--- a/activesupport/test/core_ext/range_ext_test.rb
+++ b/activesupport/test/core_ext/range_ext_test.rb
@@ -16,6 +16,11 @@ class RangeTest < ActiveSupport::TestCase
assert_equal "BETWEEN '2005-12-10 15:30:00' AND '2005-12-10 17:30:00'", date_range.to_s(:db)
end
+ def test_to_s_with_alphabets
+ alphabet_range = ("a".."z")
+ assert_equal "BETWEEN 'a' AND 'z'", alphabet_range.to_s(:db)
+ end
+
def test_to_s_with_numeric
number_range = (1..100)
assert_equal "BETWEEN '1' AND '100'", number_range.to_s(:db)
@@ -32,7 +37,7 @@ class RangeTest < ActiveSupport::TestCase
end
def test_overlaps_last_exclusive
- assert !(1...5).overlaps?(5..10)
+ assert_not (1...5).overlaps?(5..10)
end
def test_overlaps_first_inclusive
@@ -40,7 +45,7 @@ class RangeTest < ActiveSupport::TestCase
end
def test_overlaps_first_exclusive
- assert !(5..10).overlaps?(1...5)
+ assert_not (5..10).overlaps?(1...5)
end
def test_should_include_identical_inclusive
@@ -97,28 +102,31 @@ class RangeTest < ActiveSupport::TestCase
def test_no_overlaps_on_time
time_range_1 = Time.utc(2005, 12, 10, 15, 30)..Time.utc(2005, 12, 10, 17, 30)
time_range_2 = Time.utc(2005, 12, 10, 17, 31)..Time.utc(2005, 12, 10, 18, 00)
- assert !time_range_1.overlaps?(time_range_2)
+ assert_not time_range_1.overlaps?(time_range_2)
end
def test_each_on_time_with_zone
- twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"] , Time.utc(2006, 11, 28, 10, 30))
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30))
assert_raises TypeError do
((twz - 1.hour)..twz).each {}
end
end
def test_step_on_time_with_zone
- twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"] , Time.utc(2006, 11, 28, 10, 30))
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30))
assert_raises TypeError do
((twz - 1.hour)..twz).step(1) {}
end
end
def test_include_on_time_with_zone
- twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"] , Time.utc(2006, 11, 28, 10, 30))
- assert_raises TypeError do
- ((twz - 1.hour)..twz).include?(twz)
- end
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30))
+ assert ((twz - 1.hour)..twz).include?(twz)
+ end
+
+ def test_case_equals_on_time_with_zone
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30))
+ assert ((twz - 1.hour)..twz) === twz
end
def test_date_time_with_each
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index 9fd6d8ac0f..b8de16cc5e 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -9,9 +9,9 @@ require "constantize_test_cases"
require "active_support/inflector"
require "active_support/core_ext/string"
require "active_support/time"
-require "active_support/core_ext/string/strip"
require "active_support/core_ext/string/output_safety"
require "active_support/core_ext/string/indent"
+require "active_support/core_ext/string/strip"
require "time_zone_test_helpers"
require "yaml"
@@ -24,6 +24,10 @@ class StringInflectionsTest < ActiveSupport::TestCase
assert_equal "", "".strip_heredoc
end
+ def test_strip_heredoc_on_a_frozen_string
+ assert "".freeze.strip_heredoc.frozen?
+ end
+
def test_strip_heredoc_on_a_string_with_no_lines
assert_equal "x", "x".strip_heredoc
assert_equal "x", " x".strip_heredoc
@@ -197,7 +201,7 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
def test_string_parameterized_underscore_preserve_case
- StringToParameterizePreserceCaseWithUnderscore.each do |normal, slugged|
+ StringToParameterizePreserveCaseWithUnderscore.each do |normal, slugged|
assert_equal(slugged, normal.parameterize(separator: "_", preserve_case: true))
end
end
@@ -233,11 +237,11 @@ class StringInflectionsTest < ActiveSupport::TestCase
s = "hello"
assert s.starts_with?("h")
assert s.starts_with?("hel")
- assert !s.starts_with?("el")
+ assert_not s.starts_with?("el")
assert s.ends_with?("o")
assert s.ends_with?("lo")
- assert !s.ends_with?("el")
+ assert_not s.ends_with?("el")
end
def test_string_squish
@@ -259,8 +263,8 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
def test_string_inquiry
- assert "production".inquiry.production?
- assert !"production".inquiry.development?
+ assert_predicate "production".inquiry, :production?
+ assert_not_predicate "production".inquiry, :development?
end
def test_truncate
@@ -281,6 +285,68 @@ class StringInflectionsTest < ActiveSupport::TestCase
assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, omission: "[...]", separator: /\s/)
end
+ def test_truncate_bytes
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16)
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: nil)
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: " ")
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: "🖖")
+
+ assert_equal "👍👍👍…", "👍👍👍👍".truncate_bytes(15)
+ assert_equal "👍👍👍", "👍👍👍👍".truncate_bytes(15, omission: nil)
+ assert_equal "👍👍👍 ", "👍👍👍👍".truncate_bytes(15, omission: " ")
+ assert_equal "👍👍🖖", "👍👍👍👍".truncate_bytes(15, omission: "🖖")
+
+ assert_equal "…", "👍👍👍👍".truncate_bytes(5)
+ assert_equal "👍", "👍👍👍👍".truncate_bytes(5, omission: nil)
+ assert_equal "👍 ", "👍👍👍👍".truncate_bytes(5, omission: " ")
+ assert_equal "🖖", "👍👍👍👍".truncate_bytes(5, omission: "🖖")
+
+ assert_equal "…", "👍👍👍👍".truncate_bytes(4)
+ assert_equal "👍", "👍👍👍👍".truncate_bytes(4, omission: nil)
+ assert_equal " ", "👍👍👍👍".truncate_bytes(4, omission: " ")
+ assert_equal "🖖", "👍👍👍👍".truncate_bytes(4, omission: "🖖")
+
+ assert_raise ArgumentError do
+ "👍👍👍👍".truncate_bytes(3, omission: "🖖")
+ end
+ end
+
+ def test_truncate_bytes_preserves_codepoints
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16)
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: nil)
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: " ")
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: "🖖")
+
+ assert_equal "👍👍👍…", "👍👍👍👍".truncate_bytes(15)
+ assert_equal "👍👍👍", "👍👍👍👍".truncate_bytes(15, omission: nil)
+ assert_equal "👍👍👍 ", "👍👍👍👍".truncate_bytes(15, omission: " ")
+ assert_equal "👍👍🖖", "👍👍👍👍".truncate_bytes(15, omission: "🖖")
+
+ assert_equal "…", "👍👍👍👍".truncate_bytes(5)
+ assert_equal "👍", "👍👍👍👍".truncate_bytes(5, omission: nil)
+ assert_equal "👍 ", "👍👍👍👍".truncate_bytes(5, omission: " ")
+ assert_equal "🖖", "👍👍👍👍".truncate_bytes(5, omission: "🖖")
+
+ assert_equal "…", "👍👍👍👍".truncate_bytes(4)
+ assert_equal "👍", "👍👍👍👍".truncate_bytes(4, omission: nil)
+ assert_equal " ", "👍👍👍👍".truncate_bytes(4, omission: " ")
+ assert_equal "🖖", "👍👍👍👍".truncate_bytes(4, omission: "🖖")
+
+ assert_raise ArgumentError do
+ "👍👍👍👍".truncate_bytes(3, omission: "🖖")
+ end
+ end
+
+ def test_truncates_bytes_preserves_grapheme_clusters
+ assert_equal "a ", "a ❤️ b".truncate_bytes(2, omission: nil)
+ assert_equal "a ", "a ❤️ b".truncate_bytes(3, omission: nil)
+ assert_equal "a ", "a ❤️ b".truncate_bytes(7, omission: nil)
+ assert_equal "a ❤️", "a ❤️ b".truncate_bytes(8, omission: nil)
+
+ assert_equal "a ", "a 👩‍❤️‍👩".truncate_bytes(13, omission: nil)
+ assert_equal "", "👩‍❤️‍👩".truncate_bytes(13, omission: nil)
+ end
+
def test_truncate_words
assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3)
assert_equal "Hello Big...", "Hello Big World!".truncate_words(2)
@@ -317,7 +383,7 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
def test_truncate_should_not_be_html_safe
- assert !"Hello World!".truncate(12).html_safe?
+ assert_not_predicate "Hello World!".truncate(12), :html_safe?
end
def test_remove
@@ -639,7 +705,7 @@ end
class StringBehaviourTest < ActiveSupport::TestCase
def test_acts_like_string
- assert "Bambi".acts_like_string?
+ assert_predicate "Bambi", :acts_like_string?
end
end
@@ -654,10 +720,10 @@ class CoreExtStringMultibyteTest < ActiveSupport::TestCase
end
def test_string_should_recognize_utf8_strings
- assert UTF8_STRING.is_utf8?
- assert ASCII_STRING.is_utf8?
- assert !EUC_JP_STRING.is_utf8?
- assert !INVALID_UTF8_STRING.is_utf8?
+ assert_predicate UTF8_STRING, :is_utf8?
+ assert_predicate ASCII_STRING, :is_utf8?
+ assert_not_predicate EUC_JP_STRING, :is_utf8?
+ assert_not_predicate INVALID_UTF8_STRING, :is_utf8?
end
def test_mb_chars_returns_instance_of_proxy_class
@@ -676,12 +742,12 @@ class OutputSafetyTest < ActiveSupport::TestCase
end
test "A string is unsafe by default" do
- assert !@string.html_safe?
+ assert_not_predicate @string, :html_safe?
end
test "A string can be marked safe" do
string = @string.html_safe
- assert string.html_safe?
+ assert_predicate string, :html_safe?
end
test "Marking a string safe returns the string" do
@@ -689,15 +755,15 @@ class OutputSafetyTest < ActiveSupport::TestCase
end
test "An integer is safe by default" do
- assert 5.html_safe?
+ assert_predicate 5, :html_safe?
end
test "a float is safe by default" do
- assert 5.7.html_safe?
+ assert_predicate 5.7, :html_safe?
end
test "An object is unsafe by default" do
- assert !@object.html_safe?
+ assert_not_predicate @object, :html_safe?
end
test "Adding an object to a safe string returns a safe string" do
@@ -705,7 +771,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
string << @object
assert_equal "helloother", string
- assert string.html_safe?
+ assert_predicate string, :html_safe?
end
test "Adding a safe string to another safe string returns a safe string" do
@@ -714,7 +780,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
@combination = @other_string + string
assert_equal "otherhello", @combination
- assert @combination.html_safe?
+ assert_predicate @combination, :html_safe?
end
test "Adding an unsafe string to a safe string escapes it and returns a safe string" do
@@ -725,20 +791,20 @@ class OutputSafetyTest < ActiveSupport::TestCase
assert_equal "other&lt;foo&gt;", @combination
assert_equal "hello<foo>", @other_combination
- assert @combination.html_safe?
- assert !@other_combination.html_safe?
+ assert_predicate @combination, :html_safe?
+ assert_not_predicate @other_combination, :html_safe?
end
test "Prepending safe onto unsafe yields unsafe" do
@string.prepend "other".html_safe
- assert !@string.html_safe?
+ assert_not_predicate @string, :html_safe?
assert_equal "otherhello", @string
end
test "Prepending unsafe onto safe yields escaped safe" do
other = "other".html_safe
other.prepend "<foo>"
- assert other.html_safe?
+ assert_predicate other, :html_safe?
assert_equal "&lt;foo&gt;other", other
end
@@ -747,14 +813,14 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = @string.html_safe
@other_string.concat(string)
- assert !@other_string.html_safe?
+ assert_not_predicate @other_string, :html_safe?
end
test "Concatting unsafe onto safe yields escaped safe" do
@other_string = "other".html_safe
string = @other_string.concat("<foo>")
assert_equal "other&lt;foo&gt;", string
- assert string.html_safe?
+ assert_predicate string, :html_safe?
end
test "Concatting safe onto safe yields safe" do
@@ -762,7 +828,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = @string.html_safe
@other_string.concat(string)
- assert @other_string.html_safe?
+ assert_predicate @other_string, :html_safe?
end
test "Concatting safe onto unsafe with << yields unsafe" do
@@ -770,14 +836,14 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = @string.html_safe
@other_string << string
- assert !@other_string.html_safe?
+ assert_not_predicate @other_string, :html_safe?
end
test "Concatting unsafe onto safe with << yields escaped safe" do
@other_string = "other".html_safe
string = @other_string << "<foo>"
assert_equal "other&lt;foo&gt;", string
- assert string.html_safe?
+ assert_predicate string, :html_safe?
end
test "Concatting safe onto safe with << yields safe" do
@@ -785,7 +851,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = @string.html_safe
@other_string << string
- assert @other_string.html_safe?
+ assert_predicate @other_string, :html_safe?
end
test "Concatting safe onto unsafe with % yields unsafe" do
@@ -793,7 +859,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = @string.html_safe
@other_string = @other_string % string
- assert !@other_string.html_safe?
+ assert_not_predicate @other_string, :html_safe?
end
test "Concatting unsafe onto safe with % yields escaped safe" do
@@ -801,7 +867,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = @other_string % "<foo>"
assert_equal "other&lt;foo&gt;", string
- assert string.html_safe?
+ assert_predicate string, :html_safe?
end
test "Concatting safe onto safe with % yields safe" do
@@ -809,7 +875,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = @string.html_safe
@other_string = @other_string % string
- assert @other_string.html_safe?
+ assert_predicate @other_string, :html_safe?
end
test "Concatting with % doesn't modify a string" do
@@ -823,7 +889,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = @string.html_safe
string = string.concat(13)
assert_equal "hello".dup.concat(13), string
- assert string.html_safe?
+ assert_predicate string, :html_safe?
end
test "emits normal string yaml" do
@@ -832,8 +898,8 @@ class OutputSafetyTest < ActiveSupport::TestCase
test "call to_param returns a normal string" do
string = @string.html_safe
- assert string.html_safe?
- assert !string.to_param.html_safe?
+ assert_predicate string, :html_safe?
+ assert_not_predicate string.to_param, :html_safe?
end
test "ERB::Util.html_escape should escape unsafe characters" do
diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb
index dc2f4c5ac7..e1cb22fda8 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -178,10 +178,6 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005, 2, 4, 19, 30, 59, Rational(999999999, 1000)), Time.local(2005, 2, 4, 19, 30, 10).end_of_minute
end
- def test_last_year
- assert_equal Time.local(2004, 6, 5, 10), Time.local(2005, 6, 5, 10, 0, 0).last_year
- end
-
def test_ago
assert_equal Time.local(2005, 2, 22, 10, 10, 9), Time.local(2005, 2, 22, 10, 10, 10).ago(1)
assert_equal Time.local(2005, 2, 22, 9, 10, 10), Time.local(2005, 2, 22, 10, 10, 10).ago(3600)
@@ -455,7 +451,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2013, 10, 3, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, days: 5)
assert_equal Time.local(2013, 10, 17, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5)
assert_equal Time.local(2001, 12, 27, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: -3, months: -2, days: -1)
- assert_equal Time.local(2005, 2, 28, 15, 15, 10), Time.local(2004, 2, 29, 15, 15, 10).advance(years: 1) #leap day plus one year
+ assert_equal Time.local(2005, 2, 28, 15, 15, 10), Time.local(2004, 2, 29, 15, 15, 10).advance(years: 1) # leap day plus one year
assert_equal Time.local(2005, 2, 28, 20, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(hours: 5)
assert_equal Time.local(2005, 2, 28, 15, 22, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(minutes: 7)
assert_equal Time.local(2005, 2, 28, 15, 15, 19), Time.local(2005, 2, 28, 15, 15, 10).advance(seconds: 9)
@@ -477,7 +473,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.utc(2013, 10, 3, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).advance(years: 7, months: 19, days: 11)
assert_equal Time.utc(2013, 10, 17, 15, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5)
assert_equal Time.utc(2001, 12, 27, 15, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(years: -3, months: -2, days: -1)
- assert_equal Time.utc(2005, 2, 28, 15, 15, 10), Time.utc(2004, 2, 29, 15, 15, 10).advance(years: 1) #leap day plus one year
+ assert_equal Time.utc(2005, 2, 28, 15, 15, 10), Time.utc(2004, 2, 29, 15, 15, 10).advance(years: 1) # leap day plus one year
assert_equal Time.utc(2005, 2, 28, 20, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(hours: 5)
assert_equal Time.utc(2005, 2, 28, 15, 22, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(minutes: 7)
assert_equal Time.utc(2005, 2, 28, 15, 15, 19), Time.utc(2005, 2, 28, 15, 15, 10).advance(seconds: 9)
@@ -499,7 +495,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.new(2013, 10, 3, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").advance(years: 7, months: 19, days: 11)
assert_equal Time.new(2013, 10, 17, 15, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(years: 7, months: 19, weeks: 2, days: 5)
assert_equal Time.new(2001, 12, 27, 15, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(years: -3, months: -2, days: -1)
- assert_equal Time.new(2005, 2, 28, 15, 15, 10, "-08:00"), Time.new(2004, 2, 29, 15, 15, 10, "-08:00").advance(years: 1) #leap day plus one year
+ assert_equal Time.new(2005, 2, 28, 15, 15, 10, "-08:00"), Time.new(2004, 2, 29, 15, 15, 10, "-08:00").advance(years: 1) # leap day plus one year
assert_equal Time.new(2005, 2, 28, 20, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(hours: 5)
assert_equal Time.new(2005, 2, 28, 15, 22, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(minutes: 7)
assert_equal Time.new(2005, 2, 28, 15, 15, 19, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(seconds: 9)
@@ -664,10 +660,6 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
end
- def test_last_month_on_31st
- assert_equal Time.local(2004, 2, 29), Time.local(2004, 3, 31).last_month
- end
-
def test_xmlschema_is_available
assert_nothing_raised { Time.now.xmlschema }
end
@@ -745,7 +737,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_acts_like_time
- assert Time.new.acts_like_time?
+ assert_predicate Time.new, :acts_like_time?
end
def test_formatted_offset_with_utc
@@ -764,26 +756,26 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_compare_with_time
- assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999)
- assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0)
+ assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999)
+ assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0)
assert_equal(-1, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0, 001))
end
def test_compare_with_datetime
- assert_equal 1, Time.utc(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59)
- assert_equal 0, Time.utc(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0)
+ assert_equal 1, Time.utc(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59)
+ assert_equal 0, Time.utc(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0)
assert_equal(-1, Time.utc(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 1))
end
def test_compare_with_time_with_zone
- assert_equal 1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"])
- assert_equal 0, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"])
+ assert_equal 1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"])
+ assert_equal 0, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"])
assert_equal(-1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone["UTC"]))
end
def test_compare_with_string
- assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999).to_s
- assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s
+ assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999).to_s
+ assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s
assert_equal(-1, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1, 0).to_s)
assert_nil Time.utc(2000) <=> "Invalid as Time"
end
@@ -871,11 +863,11 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_minus_with_time_with_zone
- assert_equal 86_400.0, Time.utc(2000, 1, 2) - ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["UTC"])
+ assert_equal 86_400.0, Time.utc(2000, 1, 2) - ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["UTC"])
end
def test_minus_with_datetime
- assert_equal 86_400.0, Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1)
+ assert_equal 86_400.0, Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1)
end
def test_time_created_with_local_constructor_cannot_represent_times_during_hour_skipped_by_dst
@@ -883,7 +875,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
# On Apr 2 2006 at 2:00AM in US, clocks were moved forward to 3:00AM.
# Therefore, 2AM EST doesn't exist for this date; Time.local fails over to 3:00AM EDT
assert_equal Time.local(2006, 4, 2, 3), Time.local(2006, 4, 2, 2)
- assert Time.local(2006, 4, 2, 2).dst?
+ assert_predicate Time.local(2006, 4, 2, 2), :dst?
end
end
diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb
index 0f80a24758..e650209268 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -3,7 +3,6 @@
require "abstract_unit"
require "active_support/time"
require "time_zone_test_helpers"
-require "active_support/core_ext/string/strip"
require "yaml"
class TimeWithZoneTest < ActiveSupport::TestCase
@@ -50,6 +49,12 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_raise(ArgumentError) { @twz.in_time_zone(Object.new) }
end
+ def test_in_time_zone_with_ambiguous_time
+ with_env_tz "Europe/Moscow" do
+ assert_equal Time.utc(2014, 10, 25, 22, 0, 0), Time.local(2014, 10, 26, 1, 0, 0).in_time_zone("Moscow")
+ end
+ end
+
def test_localtime
assert_equal @twz.localtime, @twz.utc.getlocal
assert_instance_of Time, @twz.localtime
@@ -78,7 +83,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
def test_formatted_offset
assert_equal "-05:00", @twz.formatted_offset
- assert_equal "-04:00", ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).formatted_offset #dst
+ assert_equal "-04:00", ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).formatted_offset # dst
end
def test_dst?
@@ -88,7 +93,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
def test_zone
assert_equal "EST", @twz.zone
- assert_equal "EDT", ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).zone #dst
+ assert_equal "EDT", ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).zone # dst
end
def test_nsec
@@ -157,7 +162,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_to_yaml
- yaml = <<-EOF.strip_heredoc
+ yaml = <<~EOF
--- !ruby/object:ActiveSupport::TimeWithZone
utc: 2000-01-01 00:00:00.000000000 Z
zone: !ruby/object:ActiveSupport::TimeZone
@@ -169,7 +174,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_ruby_to_yaml
- yaml = <<-EOF.strip_heredoc
+ yaml = <<~EOF
---
twz: !ruby/object:ActiveSupport::TimeWithZone
utc: 2000-01-01 00:00:00.000000000 Z
@@ -182,7 +187,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_yaml_load
- yaml = <<-EOF.strip_heredoc
+ yaml = <<~EOF
--- !ruby/object:ActiveSupport::TimeWithZone
utc: 2000-01-01 00:00:00.000000000 Z
zone: !ruby/object:ActiveSupport::TimeZone
@@ -194,7 +199,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_ruby_yaml_load
- yaml = <<-EOF.strip_heredoc
+ yaml = <<~EOF
---
twz: !ruby/object:ActiveSupport::TimeWithZone
utc: 2000-01-01 00:00:00.000000000 Z
@@ -215,20 +220,20 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_compare_with_time
- assert_equal 1, @twz <=> Time.utc(1999, 12, 31, 23, 59, 59)
- assert_equal 0, @twz <=> Time.utc(2000, 1, 1, 0, 0, 0)
+ assert_equal 1, @twz <=> Time.utc(1999, 12, 31, 23, 59, 59)
+ assert_equal 0, @twz <=> Time.utc(2000, 1, 1, 0, 0, 0)
assert_equal(-1, @twz <=> Time.utc(2000, 1, 1, 0, 0, 1))
end
def test_compare_with_datetime
- assert_equal 1, @twz <=> DateTime.civil(1999, 12, 31, 23, 59, 59)
- assert_equal 0, @twz <=> DateTime.civil(2000, 1, 1, 0, 0, 0)
+ assert_equal 1, @twz <=> DateTime.civil(1999, 12, 31, 23, 59, 59)
+ assert_equal 0, @twz <=> DateTime.civil(2000, 1, 1, 0, 0, 0)
assert_equal(-1, @twz <=> DateTime.civil(2000, 1, 1, 0, 0, 1))
end
def test_compare_with_time_with_zone
- assert_equal 1, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"])
- assert_equal 0, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"])
+ assert_equal 1, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"])
+ assert_equal 0, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"])
assert_equal(-1, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone["UTC"]))
end
@@ -284,6 +289,20 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
end
+ def test_before
+ twz = ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone)
+ assert_equal false, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 11, 59, 59), @time_zone))
+ assert_equal false, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone))
+ assert_equal true, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 00, 1), @time_zone))
+ end
+
+ def test_after
+ twz = ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone)
+ assert_equal true, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 11, 59, 59), @time_zone))
+ assert_equal false, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone))
+ assert_equal false, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 00, 1), @time_zone))
+ end
+
def test_eql?
assert_equal true, @twz.eql?(@twz.dup)
assert_equal true, @twz.eql?(Time.utc(2000))
@@ -301,13 +320,13 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_plus_with_integer
- assert_equal Time.utc(1999, 12, 31, 19, 0 , 5), (@twz + 5).time
+ assert_equal Time.utc(1999, 12, 31, 19, 0, 5), (@twz + 5).time
end
def test_plus_with_integer_when_self_wraps_datetime
datetime = DateTime.civil(2000, 1, 1, 0)
twz = ActiveSupport::TimeWithZone.new(datetime, @time_zone)
- assert_equal DateTime.civil(1999, 12, 31, 19, 0 , 5), (twz + 5).time
+ assert_equal DateTime.civil(1999, 12, 31, 19, 0, 5), (twz + 5).time
end
def test_plus_when_crossing_time_class_limit
@@ -316,31 +335,31 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_plus_with_duration
- assert_equal Time.utc(2000, 1, 5, 19, 0 , 0), (@twz + 5.days).time
+ assert_equal Time.utc(2000, 1, 5, 19, 0, 0), (@twz + 5.days).time
end
def test_minus_with_integer
- assert_equal Time.utc(1999, 12, 31, 18, 59 , 55), (@twz - 5).time
+ assert_equal Time.utc(1999, 12, 31, 18, 59, 55), (@twz - 5).time
end
def test_minus_with_integer_when_self_wraps_datetime
datetime = DateTime.civil(2000, 1, 1, 0)
twz = ActiveSupport::TimeWithZone.new(datetime, @time_zone)
- assert_equal DateTime.civil(1999, 12, 31, 18, 59 , 55), (twz - 5).time
+ assert_equal DateTime.civil(1999, 12, 31, 18, 59, 55), (twz - 5).time
end
def test_minus_with_duration
- assert_equal Time.utc(1999, 12, 26, 19, 0 , 0), (@twz - 5.days).time
+ assert_equal Time.utc(1999, 12, 26, 19, 0, 0), (@twz - 5.days).time
end
def test_minus_with_time
- assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1)
- assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 1)
+ assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1)
+ assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 1)
end
def test_minus_with_time_precision
- assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000))
- assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000))
+ assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000))
+ assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000))
end
def test_minus_with_time_with_zone
@@ -352,20 +371,20 @@ class TimeWithZoneTest < ActiveSupport::TestCase
def test_minus_with_time_with_zone_precision
twz1 = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0, Rational(1, 1000)), ActiveSupport::TimeZone["UTC"])
twz2 = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"])
- assert_equal 86_399.999999998, twz2 - twz1
+ assert_equal 86_399.999999998, twz2 - twz1
end
def test_minus_with_datetime
- assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1)
+ assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1)
end
def test_minus_with_datetime_precision
- assert_equal 86_399.999999999, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1)
+ assert_equal 86_399.999999999, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1)
end
def test_minus_with_wrapped_datetime
- assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1)
- assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1)
+ assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1)
+ assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1)
end
def test_plus_and_minus_enforce_spring_dst_rules
@@ -475,7 +494,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_acts_like_time
- assert @twz.acts_like_time?
+ assert_predicate @twz, :acts_like_time?
assert @twz.acts_like?(:time)
assert ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone).acts_like?(:time)
end
@@ -486,7 +505,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_blank?
- assert_not @twz.blank?
+ assert_not_predicate @twz, :blank?
end
def test_is_a
@@ -501,17 +520,17 @@ class TimeWithZoneTest < ActiveSupport::TestCase
def test_method_missing_with_time_return_value
assert_instance_of ActiveSupport::TimeWithZone, @twz.months_since(1)
- assert_equal Time.utc(2000, 1, 31, 19, 0 , 0), @twz.months_since(1).time
+ assert_equal Time.utc(2000, 1, 31, 19, 0, 0), @twz.months_since(1).time
end
def test_marshal_dump_and_load
marshal_str = Marshal.dump(@twz)
mtime = Marshal.load(marshal_str)
assert_equal Time.utc(2000, 1, 1, 0), mtime.utc
- assert mtime.utc.utc?
+ assert_predicate mtime.utc, :utc?
assert_equal ActiveSupport::TimeZone["Eastern Time (US & Canada)"], mtime.time_zone
assert_equal Time.utc(1999, 12, 31, 19), mtime.time
- assert mtime.time.utc?
+ assert_predicate mtime.time, :utc?
assert_equal @twz.inspect, mtime.inspect
end
@@ -520,16 +539,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase
marshal_str = Marshal.dump(twz)
mtime = Marshal.load(marshal_str)
assert_equal Time.utc(2000, 1, 1, 0), mtime.utc
- assert mtime.utc.utc?
+ assert_predicate mtime.utc, :utc?
assert_equal "America/New_York", mtime.time_zone.name
assert_equal Time.utc(1999, 12, 31, 19), mtime.time
- assert mtime.time.utc?
+ assert_predicate mtime.time, :utc?
assert_equal @twz.inspect, mtime.inspect
end
def test_freeze
@twz.freeze
- assert @twz.frozen?
+ assert_predicate @twz, :frozen?
end
def test_freeze_preloads_instance_variables
@@ -1029,8 +1048,8 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
def test_nil_time_zone
Time.use_zone nil do
- assert !@t.in_time_zone.respond_to?(:period), "no period method"
- assert !@dt.in_time_zone.respond_to?(:period), "no period method"
+ assert_not_respond_to @t.in_time_zone, :period, "no period method"
+ assert_not_respond_to @dt.in_time_zone, :period, "no period method"
end
end
@@ -1218,7 +1237,7 @@ class TimeWithZoneMethodsForDate < ActiveSupport::TestCase
def test_nil_time_zone
with_tz_default nil do
- assert !@d.in_time_zone.respond_to?(:period), "no period method"
+ assert_not_respond_to @d.in_time_zone, :period, "no period method"
end
end
@@ -1267,9 +1286,9 @@ class TimeWithZoneMethodsForString < ActiveSupport::TestCase
def test_nil_time_zone
with_tz_default nil do
- assert !@s.in_time_zone.respond_to?(:period), "no period method"
- assert !@u.in_time_zone.respond_to?(:period), "no period method"
- assert !@z.in_time_zone.respond_to?(:period), "no period method"
+ assert_not_respond_to @s.in_time_zone, :period, "no period method"
+ assert_not_respond_to @u.in_time_zone, :period, "no period method"
+ assert_not_respond_to @z.in_time_zone, :period, "no period method"
end
end
@@ -1301,4 +1320,10 @@ class TimeWithZoneMethodsForString < ActiveSupport::TestCase
assert_raise(ArgumentError) { @u.in_time_zone(Object.new) }
assert_raise(ArgumentError) { @z.in_time_zone(Object.new) }
end
+
+ def test_in_time_zone_with_ambiguous_time
+ with_tz_default "Moscow" do
+ assert_equal Time.utc(2014, 10, 25, 22, 0, 0), "2014-10-26 01:00:00".in_time_zone
+ end
+ end
end
diff --git a/activesupport/test/core_ext/uri_ext_test.rb b/activesupport/test/core_ext/uri_ext_test.rb
index 8816b0d392..c0686bc720 100644
--- a/activesupport/test/core_ext/uri_ext_test.rb
+++ b/activesupport/test/core_ext/uri_ext_test.rb
@@ -9,6 +9,6 @@ class URIExtTest < ActiveSupport::TestCase
str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese.
parser = URI.parser
- assert_equal str, parser.unescape(parser.escape(str))
+ assert_equal str + str, parser.unescape(str + parser.escape(str).encode(Encoding::UTF_8))
end
end
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index d636da46d2..a4fbab7b55 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -185,6 +185,61 @@ class DependenciesTest < ActiveSupport::TestCase
end
end
+ # Regression see https://github.com/rails/rails/issues/31694
+ def test_included_constant_that_changes_to_have_exception_then_back_does_not_loop_forever
+ # This constant references a nested constant whose namespace will be auto-generated
+ parent_constant = <<-RUBY
+ class ConstantReloadError
+ AnotherConstant::ReloadError
+ end
+ RUBY
+
+ # This constant's namespace will be auto-generated,
+ # also, we'll edit it to contain an error at load-time
+ child_constant = <<-RUBY
+ class AnotherConstant::ReloadError
+ # no_such_method_as_this
+ end
+ RUBY
+
+ # Create a version which contains an error during loading
+ child_constant_with_error = child_constant.sub("# no_such_method_as_this", "no_such_method_as_this")
+
+ fixtures_path = File.join(__dir__, "autoloading_fixtures")
+ Dir.mktmpdir(nil, fixtures_path) do |tmpdir|
+ # Set up the file structure where constants will be loaded from
+ child_constant_path = "#{tmpdir}/another_constant/reload_error.rb"
+ File.write("#{tmpdir}/constant_reload_error.rb", parent_constant)
+ Dir.mkdir("#{tmpdir}/another_constant")
+ File.write(child_constant_path, child_constant_with_error)
+
+ tmpdir_name = tmpdir.split("/").last
+ with_loading("autoloading_fixtures/#{tmpdir_name}") do
+ # Load the file, with the error:
+ assert_raises(NameError) {
+ ConstantReloadError
+ }
+
+ Timeout.timeout(0.1) do
+ # Remove the constant, as if Rails development middleware is reloading changed files:
+ ActiveSupport::Dependencies.remove_unloadable_constants!
+ assert_not defined?(AnotherConstant::ReloadError)
+ end
+
+ # Change the file, so that it is **correct** this time:
+ File.write(child_constant_path, child_constant)
+
+ # Again: Remove the constant, as if Rails development middleware is reloading changed files:
+ ActiveSupport::Dependencies.remove_unloadable_constants!
+ assert_not defined?(AnotherConstant::ReloadError)
+
+ # Now, reload the _fixed_ constant:
+ assert ConstantReloadError
+ assert AnotherConstant::ReloadError
+ end
+ end
+ end
+
def test_module_loading
with_autoloading_fixtures do
assert_kind_of Module, A
@@ -480,9 +535,9 @@ class DependenciesTest < ActiveSupport::TestCase
def test_qualified_const_defined_should_not_call_const_missing
ModuleWithMissing.missing_count = 0
- assert ! ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A")
+ assert_not ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A")
assert_equal 0, ModuleWithMissing.missing_count
- assert ! ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A::B")
+ assert_not ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A::B")
assert_equal 0, ModuleWithMissing.missing_count
end
@@ -492,13 +547,13 @@ class DependenciesTest < ActiveSupport::TestCase
def test_autoloaded?
with_autoloading_fixtures do
- assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder")
- assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass")
+ assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder")
+ assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass")
assert ActiveSupport::Dependencies.autoloaded?(ModuleFolder)
assert ActiveSupport::Dependencies.autoloaded?("ModuleFolder")
- assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass")
+ assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass")
assert ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass)
@@ -509,11 +564,11 @@ class DependenciesTest < ActiveSupport::TestCase
assert ActiveSupport::Dependencies.autoloaded?(:ModuleFolder)
# Anonymous modules aren't autoloaded.
- assert !ActiveSupport::Dependencies.autoloaded?(Module.new)
+ assert_not ActiveSupport::Dependencies.autoloaded?(Module.new)
nil_name = Module.new
def nil_name.name() nil end
- assert !ActiveSupport::Dependencies.autoloaded?(nil_name)
+ assert_not ActiveSupport::Dependencies.autoloaded?(nil_name)
end
ensure
remove_constants(:ModuleFolder)
@@ -723,7 +778,7 @@ class DependenciesTest < ActiveSupport::TestCase
M.unloadable
ActiveSupport::Dependencies.clear
- assert ! defined?(M)
+ assert_not defined?(M)
Object.const_set :M, Module.new
ActiveSupport::Dependencies.clear
@@ -752,9 +807,9 @@ class DependenciesTest < ActiveSupport::TestCase
Object.const_set :C, Class.new { def self.before_remove_const; end }
C.unloadable
assert_called(C, :before_remove_const, times: 1) do
- assert C.respond_to?(:before_remove_const)
+ assert_respond_to C, :before_remove_const
ActiveSupport::Dependencies.clear
- assert !defined?(C)
+ assert_not defined?(C)
end
ensure
remove_constants(:C)
@@ -968,7 +1023,7 @@ class DependenciesTest < ActiveSupport::TestCase
assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!"
end
- assert !defined?(::RaisesNameError)
+ assert_not defined?(::RaisesNameError)
2.times do
assert_raise(NameError) { ::RaisesNameError }
assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!"
diff --git a/activesupport/test/deprecation/method_wrappers_test.rb b/activesupport/test/deprecation/method_wrappers_test.rb
index 04e2325754..439e117c1d 100644
--- a/activesupport/test/deprecation/method_wrappers_test.rb
+++ b/activesupport/test/deprecation/method_wrappers_test.rb
@@ -8,6 +8,16 @@ class MethodWrappersTest < ActiveSupport::TestCase
@klass = Class.new do
def new_method; "abc" end
alias_method :old_method, :new_method
+
+ protected
+
+ def new_protected_method; "abc" end
+ alias_method :old_protected_method, :new_protected_method
+
+ private
+
+ def new_private_method; "abc" end
+ alias_method :old_private_method, :new_private_method
end
end
@@ -33,4 +43,16 @@ class MethodWrappersTest < ActiveSupport::TestCase
assert_deprecated(warning, deprecator) { assert_equal "abc", @klass.new.old_method }
end
+
+ def test_deprecate_methods_protected_method
+ ActiveSupport::Deprecation.deprecate_methods(@klass, old_protected_method: :new_protected_method)
+
+ assert(@klass.protected_method_defined?(:old_protected_method))
+ end
+
+ def test_deprecate_methods_private_method
+ ActiveSupport::Deprecation.deprecate_methods(@klass, old_private_method: :new_private_method)
+
+ assert(@klass.private_method_defined?(:old_private_method))
+ end
end
diff --git a/activesupport/test/deprecation/proxy_wrappers_test.rb b/activesupport/test/deprecation/proxy_wrappers_test.rb
index 2f866775f6..9e26052fb4 100644
--- a/activesupport/test/deprecation/proxy_wrappers_test.rb
+++ b/activesupport/test/deprecation/proxy_wrappers_test.rb
@@ -9,16 +9,16 @@ class ProxyWrappersTest < ActiveSupport::TestCase
def test_deprecated_object_proxy_doesnt_wrap_falsy_objects
proxy = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(nil, "message")
- assert !proxy
+ assert_not proxy
end
def test_deprecated_instance_variable_proxy_doesnt_wrap_falsy_objects
proxy = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(nil, :waffles)
- assert !proxy
+ assert_not proxy
end
def test_deprecated_constant_proxy_doesnt_wrap_falsy_objects
proxy = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(Waffles, NewWaffles)
- assert !proxy
+ assert_not proxy
end
end
diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb
index f2267a822f..60673c032b 100644
--- a/activesupport/test/deprecation_test.rb
+++ b/activesupport/test/deprecation_test.rb
@@ -158,7 +158,7 @@ class DeprecationTest < ActiveSupport::TestCase
stderr_output = capture(:stderr) {
assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "gem")
}
- assert stderr_output.empty?
+ assert_empty stderr_output
end
def test_default_notify_behavior
diff --git a/activesupport/test/descendants_tracker_test_cases.rb b/activesupport/test/descendants_tracker_test_cases.rb
index 1f8b4a8605..2c94c3c56c 100644
--- a/activesupport/test/descendants_tracker_test_cases.rb
+++ b/activesupport/test/descendants_tracker_test_cases.rb
@@ -37,7 +37,7 @@ module DescendantsTrackerTestCases
mark_as_autoloaded(*ALL) do
ActiveSupport::DescendantsTracker.clear
ALL.each do |k|
- assert ActiveSupport::DescendantsTracker.descendants(k).empty?
+ assert_empty ActiveSupport::DescendantsTracker.descendants(k)
end
end
end
diff --git a/activesupport/test/descendants_tracker_with_autoloading_test.rb b/activesupport/test/descendants_tracker_with_autoloading_test.rb
index 7c396b7c8e..d4fedb5a67 100644
--- a/activesupport/test/descendants_tracker_with_autoloading_test.rb
+++ b/activesupport/test/descendants_tracker_with_autoloading_test.rb
@@ -12,7 +12,7 @@ class DescendantsTrackerWithAutoloadingTest < ActiveSupport::TestCase
mark_as_autoloaded(*ALL) do
ActiveSupport::DescendantsTracker.clear
ALL.each do |k|
- assert ActiveSupport::DescendantsTracker.descendants(k).empty?
+ assert_empty ActiveSupport::DescendantsTracker.descendants(k)
end
end
end
diff --git a/activesupport/test/descendants_tracker_without_autoloading_test.rb b/activesupport/test/descendants_tracker_without_autoloading_test.rb
index f5c6a3045d..c65f69cba3 100644
--- a/activesupport/test/descendants_tracker_without_autoloading_test.rb
+++ b/activesupport/test/descendants_tracker_without_autoloading_test.rb
@@ -13,7 +13,7 @@ class DescendantsTrackerWithoutAutoloadingTest < ActiveSupport::TestCase
parent_instance = Parent.new
parent_instance.singleton_class.descendants
ActiveSupport::DescendantsTracker.clear
- assert !ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class)
+ assert_not ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class)
end
end
end
diff --git a/activesupport/test/digest_test.rb b/activesupport/test/digest_test.rb
new file mode 100644
index 0000000000..83ff2a8d83
--- /dev/null
+++ b/activesupport/test/digest_test.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "openssl"
+
+class DigestTest < ActiveSupport::TestCase
+ class InvalidDigest; end
+ def test_with_default_hash_digest_class
+ assert_equal ::Digest::MD5.hexdigest("hello friend"), ActiveSupport::Digest.hexdigest("hello friend")
+ end
+
+ def test_with_custom_hash_digest_class
+ original_hash_digest_class = ActiveSupport::Digest.hash_digest_class
+
+ ActiveSupport::Digest.hash_digest_class = ::Digest::SHA1
+ digest = ActiveSupport::Digest.hexdigest("hello friend")
+
+ assert_equal 32, digest.length
+ assert_equal ::Digest::SHA1.hexdigest("hello friend")[0...32], digest
+ ensure
+ ActiveSupport::Digest.hash_digest_class = original_hash_digest_class
+ end
+
+ def test_should_raise_argument_error_if_custom_digest_is_missing_hexdigest_method
+ assert_raises(ArgumentError) { ActiveSupport::Digest.hash_digest_class = InvalidDigest }
+ end
+end
diff --git a/activesupport/test/encrypted_configuration_test.rb b/activesupport/test/encrypted_configuration_test.rb
index 53ea9e393f..93ccf457de 100644
--- a/activesupport/test/encrypted_configuration_test.rb
+++ b/activesupport/test/encrypted_configuration_test.rb
@@ -10,8 +10,10 @@ class EncryptedConfigurationTest < ActiveSupport::TestCase
@credentials_key_path = File.join(Dir.tmpdir, "master.key")
File.write(@credentials_key_path, ActiveSupport::EncryptedConfiguration.generate_key)
- @credentials = ActiveSupport::EncryptedConfiguration.new \
- config_path: @credentials_config_path, key_path: @credentials_key_path, env_key: "RAILS_MASTER_KEY"
+ @credentials = ActiveSupport::EncryptedConfiguration.new(
+ config_path: @credentials_config_path, key_path: @credentials_key_path,
+ env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true
+ )
end
teardown do
@@ -51,15 +53,15 @@ class EncryptedConfigurationTest < ActiveSupport::TestCase
assert_equal "things", @credentials[:new]
end
+ test "raise error when writing an invalid format value" do
+ assert_raise(Psych::SyntaxError) do
+ @credentials.change do |config_file|
+ config_file.write "login: *login\n username: dummy"
+ end
+ end
+ end
+
test "raises key error when accessing config via bang method" do
assert_raise(KeyError) { @credentials.something! }
end
-
- private
- def new_credentials_configuration
- ActiveSupport::EncryptedConfiguration.new \
- config_path: @credentials_config_path,
- key_path: @credentials_key_path,
- env_key: "RAILS_MASTER_KEY"
- end
end
diff --git a/activesupport/test/encrypted_file_test.rb b/activesupport/test/encrypted_file_test.rb
index 7259726d08..ba3bbef903 100644
--- a/activesupport/test/encrypted_file_test.rb
+++ b/activesupport/test/encrypted_file_test.rb
@@ -12,8 +12,9 @@ class EncryptedFileTest < ActiveSupport::TestCase
@key_path = File.join(Dir.tmpdir, "content.txt.key")
File.write(@key_path, ActiveSupport::EncryptedFile.generate_key)
- @encrypted_file = ActiveSupport::EncryptedFile.new \
- content_path: @content_path, key_path: @key_path, env_key: "CONTENT_KEY"
+ @encrypted_file = ActiveSupport::EncryptedFile.new(
+ content_path: @content_path, key_path: @key_path, env_key: "CONTENT_KEY", raise_if_missing_key: true
+ )
end
teardown do
@@ -47,4 +48,12 @@ class EncryptedFileTest < ActiveSupport::TestCase
assert_equal "#{@content} and went by the lake", @encrypted_file.read
end
+
+ test "raise MissingKeyError when key is missing" do
+ assert_raise(ActiveSupport::EncryptedFile::MissingKeyError) do
+ ActiveSupport::EncryptedFile.new(
+ content_path: @content_path, key_path: "", env_key: "", raise_if_missing_key: true
+ ).read
+ end
+ end
end
diff --git a/activesupport/test/evented_file_update_checker_test.rb b/activesupport/test/evented_file_update_checker_test.rb
index 9b560f7f42..d3af0dbef3 100644
--- a/activesupport/test/evented_file_update_checker_test.rb
+++ b/activesupport/test/evented_file_update_checker_test.rb
@@ -39,18 +39,18 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase
FileUtils.touch(tmpfiles)
checker = new_checker(tmpfiles) {}
- assert !checker.updated?
+ assert_not_predicate checker, :updated?
# Pipes used for flow control across fork.
boot_reader, boot_writer = IO.pipe
touch_reader, touch_writer = IO.pipe
pid = fork do
- assert checker.updated?
+ assert_predicate checker, :updated?
# Clear previous check value.
checker.execute
- assert !checker.updated?
+ assert_not_predicate checker, :updated?
# Fork is booted, ready for file to be touched
# notify parent process.
@@ -60,7 +60,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase
# has been touched.
IO.select([touch_reader])
- assert checker.updated?
+ assert_predicate checker, :updated?
end
assert pid
@@ -72,7 +72,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase
# Notify fork that files have been touched.
touch_writer.write("touched")
- assert checker.updated?
+ assert_predicate checker, :updated?
Process.wait(pid)
end
diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb
index f8266dac06..72683816b3 100644
--- a/activesupport/test/file_update_checker_shared_tests.rb
+++ b/activesupport/test/file_update_checker_shared_tests.rb
@@ -30,7 +30,7 @@ module FileUpdateCheckerSharedTests
checker = new_checker { i += 1 }
- assert !checker.execute_if_updated
+ assert_not checker.execute_if_updated
assert_equal 0, i
end
@@ -41,7 +41,7 @@ module FileUpdateCheckerSharedTests
checker = new_checker(tmpfiles) { i += 1 }
- assert !checker.execute_if_updated
+ assert_not checker.execute_if_updated
assert_equal 0, i
end
@@ -89,12 +89,12 @@ module FileUpdateCheckerSharedTests
i = 0
checker = new_checker(tmpfiles) { i += 1 }
- assert !checker.updated?
+ assert_not_predicate checker, :updated?
touch(tmpfiles)
wait
- assert checker.updated?
+ assert_predicate checker, :updated?
end
test "updated should become true when watched files are modified" do
@@ -103,12 +103,12 @@ module FileUpdateCheckerSharedTests
FileUtils.touch(tmpfiles)
checker = new_checker(tmpfiles) { i += 1 }
- assert !checker.updated?
+ assert_not_predicate checker, :updated?
touch(tmpfiles)
wait
- assert checker.updated?
+ assert_predicate checker, :updated?
end
test "updated should become true when watched files are deleted" do
@@ -117,12 +117,12 @@ module FileUpdateCheckerSharedTests
FileUtils.touch(tmpfiles)
checker = new_checker(tmpfiles) { i += 1 }
- assert !checker.updated?
+ assert_not_predicate checker, :updated?
rm_f(tmpfiles)
wait
- assert checker.updated?
+ assert_predicate checker, :updated?
end
test "should be robust to handle files with wrong modified time" do
@@ -164,14 +164,14 @@ module FileUpdateCheckerSharedTests
i = 0
checker = new_checker(tmpfiles) { i += 1 }
- assert !checker.updated?
+ assert_not_predicate checker, :updated?
touch(tmpfiles)
wait
- assert checker.updated?
+ assert_predicate checker, :updated?
checker.execute
- assert !checker.updated?
+ assert_not_predicate checker, :updated?
end
test "should execute the block if files change in a watched directory one extension" do
@@ -212,7 +212,7 @@ module FileUpdateCheckerSharedTests
touch(tmpfile("foo.rb"))
wait
- assert !checker.execute_if_updated
+ assert_not checker.execute_if_updated
assert_equal 0, i
end
@@ -238,7 +238,7 @@ module FileUpdateCheckerSharedTests
mkdir(subdir)
wait
- assert !checker.execute_if_updated
+ assert_not checker.execute_if_updated
assert_equal 0, i
touch(File.join(subdir, "nested.rb"))
@@ -259,7 +259,7 @@ module FileUpdateCheckerSharedTests
touch(tmpfile("new.txt"))
wait
- assert !checker.execute_if_updated
+ assert_not checker.execute_if_updated
assert_equal 0, i
# subdir does not look for Ruby files, but its parent tmpdir does.
diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb
index b3788ee65c..a20c428bf8 100644
--- a/activesupport/test/hash_with_indifferent_access_test.rb
+++ b/activesupport/test/hash_with_indifferent_access_test.rb
@@ -169,8 +169,6 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase
end
def test_indifferent_fetch_values
- skip unless Hash.method_defined?(:fetch_values)
-
@mixed = @mixed.with_indifferent_access
assert_equal [1, 2], @mixed.fetch_values("a", "b")
@@ -282,7 +280,7 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase
replaced = hash.replace(b: 12)
assert hash.key?("b")
- assert !hash.key?(:a)
+ assert_not hash.key?(:a)
assert_equal 12, hash[:b]
assert_same hash, replaced
end
@@ -294,7 +292,7 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase
replaced = hash.replace(HashByConversion.new(b: 12))
assert hash.key?("b")
- assert !hash.key?(:a)
+ assert_not hash.key?(:a)
assert_equal 12, hash[:b]
assert_same hash, replaced
end
@@ -399,6 +397,49 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase
assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
end
+ def test_indifferent_transform_keys
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).transform_keys { |k| k * 2 }
+
+ assert_equal({ "aa" => 1, "bb" => 2 }, hash)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).transform_keys { |k| k.to_sym }
+
+ assert_equal(1, hash[:a])
+ assert_equal(1, hash["a"])
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+ end
+
+ def test_indifferent_transform_keys_bang
+ indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
+ indifferent_strings.transform_keys! { |k| k * 2 }
+
+ assert_equal({ "aa" => 1, "bb" => 2 }, indifferent_strings)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
+
+ indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
+ indifferent_strings.transform_keys! { |k| k.to_sym }
+
+ assert_equal(1, indifferent_strings[:a])
+ assert_equal(1, indifferent_strings["a"])
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
+ end
+
+ def test_indifferent_transform_values
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).transform_values { |v| v * 2 }
+
+ assert_equal({ "a" => 2, "b" => 4 }, hash)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+ end
+
+ def test_indifferent_transform_values_bang
+ indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
+ indifferent_strings.transform_values! { |v| v * 2 }
+
+ assert_equal({ "a" => 2, "b" => 4 }, indifferent_strings)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
+ end
+
def test_indifferent_compact
hash_contain_nil_value = @strings.merge("z" => nil)
hash = ActiveSupport::HashWithIndifferentAccess.new(hash_contain_nil_value)
@@ -532,7 +573,6 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase
end
def test_nested_dig_indifferent_access
- skip if RUBY_VERSION < "2.3.0"
data = { "this" => { "views" => 1234 } }.with_indifferent_access
assert_equal 1234, data.dig(:this, :views)
end
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index eeec0ab1a5..5e50acf5db 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -224,6 +224,12 @@ class InflectorTest < ActiveSupport::TestCase
assert_equal("json_html_api", ActiveSupport::Inflector.underscore("JSONHTMLAPI"))
end
+ def test_acronym_regexp_is_deprecated
+ assert_deprecated do
+ ActiveSupport::Inflector.inflections.acronym_regex
+ end
+ end
+
def test_underscore
CamelToUnderscore.each do |camel, underscore|
assert_equal(underscore, ActiveSupport::Inflector.underscore(camel))
@@ -356,6 +362,19 @@ class InflectorTest < ActiveSupport::TestCase
assert_equal("Col rpted bugs", ActiveSupport::Inflector.humanize("COL_rpted_bugs"))
end
+ def test_humanize_with_acronyms
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.acronym "LAX"
+ inflect.acronym "SFO"
+ end
+ assert_equal("LAX roundtrip to SFO", ActiveSupport::Inflector.humanize("LAX ROUNDTRIP TO SFO"))
+ assert_equal("LAX roundtrip to SFO", ActiveSupport::Inflector.humanize("LAX ROUNDTRIP TO SFO", capitalize: false))
+ assert_equal("LAX roundtrip to SFO", ActiveSupport::Inflector.humanize("lax roundtrip to sfo"))
+ assert_equal("LAX roundtrip to SFO", ActiveSupport::Inflector.humanize("lax roundtrip to sfo", capitalize: false))
+ assert_equal("LAX roundtrip to SFO", ActiveSupport::Inflector.humanize("Lax Roundtrip To Sfo"))
+ assert_equal("LAX roundtrip to SFO", ActiveSupport::Inflector.humanize("Lax Roundtrip To Sfo", capitalize: false))
+ end
+
def test_constantize
run_constantize_tests_on do |string|
ActiveSupport::Inflector.constantize(string)
@@ -441,12 +460,12 @@ class InflectorTest < ActiveSupport::TestCase
ActiveSupport::Inflector.inflections(:es) { |inflect| inflect.clear }
- assert ActiveSupport::Inflector.inflections(:es).plurals.empty?
- assert ActiveSupport::Inflector.inflections(:es).singulars.empty?
- assert ActiveSupport::Inflector.inflections(:es).uncountables.empty?
- assert !ActiveSupport::Inflector.inflections.plurals.empty?
- assert !ActiveSupport::Inflector.inflections.singulars.empty?
- assert !ActiveSupport::Inflector.inflections.uncountables.empty?
+ assert_empty ActiveSupport::Inflector.inflections(:es).plurals
+ assert_empty ActiveSupport::Inflector.inflections(:es).singulars
+ assert_empty ActiveSupport::Inflector.inflections(:es).uncountables
+ assert_not_empty ActiveSupport::Inflector.inflections.plurals
+ assert_not_empty ActiveSupport::Inflector.inflections.singulars
+ assert_not_empty ActiveSupport::Inflector.inflections.uncountables
end
def test_clear_all
@@ -459,10 +478,10 @@ class InflectorTest < ActiveSupport::TestCase
inflect.clear :all
- assert inflect.plurals.empty?
- assert inflect.singulars.empty?
- assert inflect.uncountables.empty?
- assert inflect.humans.empty?
+ assert_empty inflect.plurals
+ assert_empty inflect.singulars
+ assert_empty inflect.uncountables
+ assert_empty inflect.humans
end
end
@@ -476,10 +495,10 @@ class InflectorTest < ActiveSupport::TestCase
inflect.clear
- assert inflect.plurals.empty?
- assert inflect.singulars.empty?
- assert inflect.uncountables.empty?
- assert inflect.humans.empty?
+ assert_empty inflect.plurals
+ assert_empty inflect.singulars
+ assert_empty inflect.uncountables
+ assert_empty inflect.humans
end
end
diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb
index f1214671ce..689370cccf 100644
--- a/activesupport/test/inflector_test_cases.rb
+++ b/activesupport/test/inflector_test_cases.rb
@@ -221,7 +221,7 @@ module InflectorTestCases
"Test with malformed utf8 \251" => "test_with_malformed_utf8"
}
- StringToParameterizePreserceCaseWithUnderscore = {
+ StringToParameterizePreserveCaseWithUnderscore = {
"Donald E. Knuth" => "Donald_E_Knuth",
"Random text with *(bad)* characters" => "Random_text_with_bad_characters",
"With-some-dashes" => "With-some-dashes",
diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb
index eafa2e1712..340a2abf75 100644
--- a/activesupport/test/json/encoding_test.rb
+++ b/activesupport/test/json/encoding_test.rb
@@ -187,7 +187,7 @@ class TestJSONEncoding < ActiveSupport::TestCase
def test_array_should_pass_encoding_options_to_children_in_as_json
people = [
{ name: "John", address: { city: "London", country: "UK" } },
- { name: "Jean", address: { city: "Paris" , country: "France" } }
+ { name: "Jean", address: { city: "Paris", country: "France" } }
]
json = people.as_json only: [:address, :city]
expected = [
@@ -201,7 +201,7 @@ class TestJSONEncoding < ActiveSupport::TestCase
def test_array_should_pass_encoding_options_to_children_in_to_json
people = [
{ name: "John", address: { city: "London", country: "UK" } },
- { name: "Jean", address: { city: "Paris" , country: "France" } }
+ { name: "Jean", address: { city: "Paris", country: "France" } }
]
json = people.to_json only: [:address, :city]
@@ -210,10 +210,10 @@ class TestJSONEncoding < ActiveSupport::TestCase
People = Class.new(BasicObject) do
include Enumerable
- def initialize()
+ def initialize
@people = [
{ name: "John", address: { city: "London", country: "UK" } },
- { name: "Jean", address: { city: "Paris" , country: "France" } }
+ { name: "Jean", address: { city: "Paris", country: "France" } }
]
end
def each(*, &blk)
@@ -454,6 +454,10 @@ EXPECTED
assert_equal '{"number":null}', NaNNumber.new.to_json
end
+ def test_to_json_works_on_io_objects
+ assert_equal STDOUT.to_s.to_json, STDOUT.to_json
+ end
+
private
def object_keys(json_object)
diff --git a/activesupport/test/key_generator_test.rb b/activesupport/test/key_generator_test.rb
index a948cfbd8e..cdde2c573a 100644
--- a/activesupport/test/key_generator_test.rb
+++ b/activesupport/test/key_generator_test.rb
@@ -36,13 +36,13 @@ else
# key would break.
expected = "b129376f68f1ecae788d7433310249d65ceec090ecacd4c872a3a9e9ec78e055739be5cc6956345d5ae38e7e1daa66f1de587dc8da2bf9e8b965af4b3918a122"
- assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64).generate_key("some_salt").unpack("H*").first
+ assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64).generate_key("some_salt").unpack1("H*")
expected = "b129376f68f1ecae788d7433310249d65ceec090ecacd4c872a3a9e9ec78e055"
- assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64).generate_key("some_salt", 32).unpack("H*").first
+ assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64).generate_key("some_salt", 32).unpack1("H*")
expected = "cbea7f7f47df705967dc508f4e446fd99e7797b1d70011c6899cd39bbe62907b8508337d678505a7dc8184e037f1003ba3d19fc5d829454668e91d2518692eae"
- assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64, iterations: 2).generate_key("some_salt").unpack("H*").first
+ assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64, iterations: 2).generate_key("some_salt").unpack1("H*")
end
end
diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb
index 1fbe655642..9edf07f762 100644
--- a/activesupport/test/message_encryptor_test.rb
+++ b/activesupport/test/message_encryptor_test.rb
@@ -115,6 +115,72 @@ class MessageEncryptorTest < ActiveSupport::TestCase
assert_equal "Ruby on Rails", encryptor.decrypt_and_verify(encrypted_message)
end
+ def test_rotating_secret
+ old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], cipher: "aes-256-gcm").encrypt_and_sign("old")
+
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
+ encryptor.rotate secrets[:old]
+
+ assert_equal "old", encryptor.decrypt_and_verify(old_message)
+ end
+
+ def test_rotating_serializer
+ old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], cipher: "aes-256-gcm", serializer: JSON).
+ encrypt_and_sign(ahoy: :hoy)
+
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm", serializer: JSON)
+ encryptor.rotate secrets[:old]
+
+ assert_equal({ "ahoy" => "hoy" }, encryptor.decrypt_and_verify(old_message))
+ end
+
+ def test_rotating_aes_cbc_secrets
+ old_encryptor = ActiveSupport::MessageEncryptor.new(secrets[:old], "old sign", cipher: "aes-256-cbc")
+ old_message = old_encryptor.encrypt_and_sign("old")
+
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret)
+ encryptor.rotate secrets[:old], "old sign", cipher: "aes-256-cbc"
+
+ assert_equal "old", encryptor.decrypt_and_verify(old_message)
+ end
+
+ def test_multiple_rotations
+ older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign("older")
+ old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], "old sign").encrypt_and_sign("old")
+
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret)
+ encryptor.rotate secrets[:old], "old sign"
+ encryptor.rotate secrets[:older], "older sign"
+
+ assert_equal "new", encryptor.decrypt_and_verify(encryptor.encrypt_and_sign("new"))
+ assert_equal "old", encryptor.decrypt_and_verify(old_message)
+ assert_equal "older", encryptor.decrypt_and_verify(older_message)
+ end
+
+ def test_on_rotation_is_called_and_returns_modified_messages
+ older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign(encoded: "message")
+
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret)
+ encryptor.rotate secrets[:old]
+ encryptor.rotate secrets[:older], "older sign"
+
+ rotated = false
+ message = encryptor.decrypt_and_verify(older_message, on_rotation: proc { rotated = true })
+
+ assert_equal({ encoded: "message" }, message)
+ assert rotated
+ end
+
+ def test_with_rotated_metadata
+ old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], cipher: "aes-256-gcm").
+ encrypt_and_sign("metadata", purpose: :rotation)
+
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
+ encryptor.rotate secrets[:old]
+
+ assert_equal "metadata", encryptor.decrypt_and_verify(old_message, purpose: :rotation)
+ end
+
private
def assert_aead_not_decrypted(encryptor, value)
assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do
@@ -134,6 +200,10 @@ class MessageEncryptorTest < ActiveSupport::TestCase
end
end
+ def secrets
+ @secrets ||= Hash.new { |h, k| h[k] = SecureRandom.random_bytes(32) }
+ end
+
def munge(base64_string)
bits = ::Base64.strict_decode64(base64_string)
bits.reverse!
diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb
index fbeafca203..0fa53695e0 100644
--- a/activesupport/test/message_verifier_test.rb
+++ b/activesupport/test/message_verifier_test.rb
@@ -20,16 +20,17 @@ class MessageVerifierTest < ActiveSupport::TestCase
def setup
@verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!")
@data = { some: "data", now: Time.utc(2010) }
+ @secret = SecureRandom.random_bytes(32)
end
def test_valid_message
data, hash = @verifier.generate(@data).split("--")
- assert !@verifier.valid_message?(nil)
- assert !@verifier.valid_message?("")
- assert !@verifier.valid_message?("\xff") # invalid encoding
- assert !@verifier.valid_message?("#{data.reverse}--#{hash}")
- assert !@verifier.valid_message?("#{data}--#{hash.reverse}")
- assert !@verifier.valid_message?("purejunk")
+ assert_not @verifier.valid_message?(nil)
+ assert_not @verifier.valid_message?("")
+ assert_not @verifier.valid_message?("\xff") # invalid encoding
+ assert_not @verifier.valid_message?("#{data.reverse}--#{hash}")
+ assert_not @verifier.valid_message?("#{data}--#{hash.reverse}")
+ assert_not @verifier.valid_message?("purejunk")
end
def test_simple_round_tripping
@@ -39,7 +40,7 @@ class MessageVerifierTest < ActiveSupport::TestCase
end
def test_verified_returns_false_on_invalid_message
- assert !@verifier.verified("purejunk")
+ assert_not @verifier.verified("purejunk")
end
def test_verify_exception_on_invalid_message
@@ -90,6 +91,51 @@ class MessageVerifierTest < ActiveSupport::TestCase
signed_message = "BAh7BzoJc29tZUkiCWRhdGEGOgZFVDoIbm93SXU6CVRpbWUNIIAbgAAAAAAHOgtvZmZzZXRpADoJem9uZUkiCFVUQwY7BkY=--d03c52c91dfe4ccc5159417c660461bcce005e96"
assert_equal @data, @verifier.verify(signed_message)
end
+
+ def test_rotating_secret
+ old_message = ActiveSupport::MessageVerifier.new("old", digest: "SHA1").generate("old")
+
+ verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA1")
+ verifier.rotate "old"
+
+ assert_equal "old", verifier.verified(old_message)
+ end
+
+ def test_multiple_rotations
+ old_message = ActiveSupport::MessageVerifier.new("old", digest: "SHA256").generate("old")
+ older_message = ActiveSupport::MessageVerifier.new("older", digest: "SHA1").generate("older")
+
+ verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512")
+ verifier.rotate "old", digest: "SHA256"
+ verifier.rotate "older", digest: "SHA1"
+
+ assert_equal "new", verifier.verified(verifier.generate("new"))
+ assert_equal "old", verifier.verified(old_message)
+ assert_equal "older", verifier.verified(older_message)
+ end
+
+ def test_on_rotation_is_called_and_verified_returns_message
+ older_message = ActiveSupport::MessageVerifier.new("older", digest: "SHA1").generate(encoded: "message")
+
+ verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512")
+ verifier.rotate "old", digest: "SHA256"
+ verifier.rotate "older", digest: "SHA1"
+
+ rotated = false
+ message = verifier.verified(older_message, on_rotation: proc { rotated = true })
+
+ assert_equal({ encoded: "message" }, message)
+ assert rotated
+ end
+
+ def test_rotations_with_metadata
+ old_message = ActiveSupport::MessageVerifier.new("old").generate("old", purpose: :rotation)
+
+ verifier = ActiveSupport::MessageVerifier.new(@secret)
+ verifier.rotate "old"
+
+ assert_equal "old", verifier.verified(old_message, purpose: :rotation)
+ end
end
class MessageVerifierMetadataTest < ActiveSupport::TestCase
diff --git a/activesupport/test/messages/rotation_configuration_test.rb b/activesupport/test/messages/rotation_configuration_test.rb
new file mode 100644
index 0000000000..2f6824ed21
--- /dev/null
+++ b/activesupport/test/messages/rotation_configuration_test.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/messages/rotation_configuration"
+
+class MessagesRotationConfiguration < ActiveSupport::TestCase
+ def setup
+ @config = ActiveSupport::Messages::RotationConfiguration.new
+ end
+
+ def test_signed_configurations
+ @config.rotate :signed, "older secret", salt: "salt", digest: "SHA1"
+ @config.rotate :signed, "old secret", salt: "salt", digest: "SHA256"
+
+ assert_equal [
+ [ "older secret", salt: "salt", digest: "SHA1" ],
+ [ "old secret", salt: "salt", digest: "SHA256" ] ], @config.signed
+ end
+
+ def test_encrypted_configurations
+ @config.rotate :encrypted, "old raw key", cipher: "aes-256-gcm"
+
+ assert_equal [ [ "old raw key", cipher: "aes-256-gcm" ] ], @config.encrypted
+ end
+end
diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb
index f51fbe2671..061446c782 100644
--- a/activesupport/test/multibyte_chars_test.rb
+++ b/activesupport/test/multibyte_chars_test.rb
@@ -75,7 +75,7 @@ class MultibyteCharsTest < ActiveSupport::TestCase
def test_consumes_utf8_strings
assert @proxy_class.consumes?(UNICODE_STRING)
assert @proxy_class.consumes?(ASCII_STRING)
- assert !@proxy_class.consumes?(BYTE_STRING)
+ assert_not @proxy_class.consumes?(BYTE_STRING)
end
def test_concatenation_should_return_a_proxy_class_instance
@@ -148,7 +148,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
def test_identity
assert_equal @chars, @chars
assert @chars.eql?(@chars)
- assert !@chars.eql?(UNICODE_STRING)
+ assert_not @chars.eql?(UNICODE_STRING)
end
def test_string_methods_are_chainable
@@ -469,10 +469,10 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
end
def test_respond_to_knows_which_methods_the_proxy_responds_to
- assert "".mb_chars.respond_to?(:slice) # Defined on Chars
- assert "".mb_chars.respond_to?(:capitalize!) # Defined on Chars
- assert "".mb_chars.respond_to?(:gsub) # Defined on String
- assert !"".mb_chars.respond_to?(:undefined_method) # Not defined
+ assert_respond_to "".mb_chars, :slice # Defined on Chars
+ assert_respond_to "".mb_chars, :capitalize! # Defined on Chars
+ assert_respond_to "".mb_chars, :gsub # Defined on String
+ assert_not_respond_to "".mb_chars, :undefined_method # Not defined
end
def test_method_works_for_proxyed_methods
@@ -485,7 +485,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
end
def test_acts_like_string
- assert "Bambi".mb_chars.acts_like_string?
+ assert_predicate "Bambi".mb_chars, :acts_like_string?
end
end
diff --git a/activesupport/test/multibyte_unicode_database_test.rb b/activesupport/test/multibyte_unicode_database_test.rb
deleted file mode 100644
index 540a34493d..0000000000
--- a/activesupport/test/multibyte_unicode_database_test.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require "abstract_unit"
-
-class MultibyteUnicodeDatabaseTest < ActiveSupport::TestCase
- include ActiveSupport::Multibyte::Unicode
-
- def setup
- @ucd = UnicodeDatabase.new
- end
-
- UnicodeDatabase::ATTRIBUTES.each do |attribute|
- define_method "test_lazy_loading_on_attribute_access_of_#{attribute}" do
- assert_called(@ucd, :load) do
- @ucd.send(attribute)
- end
- end
- end
-
- def test_load
- @ucd.load
- UnicodeDatabase::ATTRIBUTES.each do |attribute|
- assert @ucd.send(attribute).length > 1
- end
- end
-end
diff --git a/activesupport/test/notifications/instrumenter_test.rb b/activesupport/test/notifications/instrumenter_test.rb
index 1d76c91d30..d5c9e82e9f 100644
--- a/activesupport/test/notifications/instrumenter_test.rb
+++ b/activesupport/test/notifications/instrumenter_test.rb
@@ -47,13 +47,13 @@ module ActiveSupport
def test_start
instrumenter.start("foo", payload)
assert_equal [["foo", instrumenter.id, payload]], notifier.starts
- assert_predicate notifier.finishes, :empty?
+ assert_empty notifier.finishes
end
def test_finish
instrumenter.finish("foo", payload)
assert_equal [["foo", instrumenter.id, payload]], notifier.finishes
- assert_predicate notifier.starts, :empty?
+ assert_empty notifier.starts
end
end
end
diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb
index 5cfbd60131..d035f993f7 100644
--- a/activesupport/test/notifications_test.rb
+++ b/activesupport/test/notifications_test.rb
@@ -250,8 +250,8 @@ module Notifications
time = Time.now
event = event(:foo, time, time + 0.01, random_id, {})
- assert_equal :foo, event.name
- assert_equal time, event.time
+ assert_equal :foo, event.name
+ assert_equal time, event.time
assert_in_delta 10.0, event.duration, 0.00001
end
@@ -270,9 +270,9 @@ module Notifications
parent.children << child
assert parent.parent_of?(child)
- assert !child.parent_of?(parent)
- assert !parent.parent_of?(not_child)
- assert !not_child.parent_of?(parent)
+ assert_not child.parent_of?(parent)
+ assert_not parent.parent_of?(not_child)
+ assert_not not_child.parent_of?(parent)
end
private
diff --git a/activesupport/test/number_helper_i18n_test.rb b/activesupport/test/number_helper_i18n_test.rb
index 6d6958be49..365fa96f4d 100644
--- a/activesupport/test/number_helper_i18n_test.rb
+++ b/activesupport/test/number_helper_i18n_test.rb
@@ -35,7 +35,7 @@ module ActiveSupport
thousand: "t",
million: "m",
billion: "b",
- trillion: "t" ,
+ trillion: "t",
quadrillion: "q"
}
}
@@ -77,10 +77,10 @@ module ActiveSupport
end
def test_number_with_i18n_precision
- #Delimiter was set to ""
+ # Delimiter was set to ""
assert_equal("10000", number_to_rounded(10000, locale: "ts"))
- #Precision inherited and significant was set
+ # Precision inherited and significant was set
assert_equal("1.00", number_to_rounded(1.0, locale: "ts"))
end
@@ -90,7 +90,7 @@ module ActiveSupport
end
def test_number_with_i18n_delimiter
- #Delimiter "," and separator "."
+ # Delimiter "," and separator "."
assert_equal("1,000,000.234", number_to_delimited(1000000.234, locale: "ts"))
end
@@ -114,7 +114,7 @@ module ActiveSupport
end
def test_number_to_i18n_human_size
- #b for bytes and k for kbytes
+ # b for bytes and k for kbytes
assert_equal("2 k", number_to_human_size(2048, locale: "ts"))
assert_equal("42 b", number_to_human_size(42, locale: "ts"))
end
@@ -125,11 +125,11 @@ module ActiveSupport
end
def test_number_to_human_with_default_translation_scope
- #Using t for thousand
+ # Using t for thousand
assert_equal "2 t", number_to_human(2000, locale: "ts")
- #Significant was set to true with precision 2, using b for billion
+ # Significant was set to true with precision 2, using b for billion
assert_equal "1.2 b", number_to_human(1234567890, locale: "ts")
- #Using pluralization (Ten/Tens and Tenth/Tenths)
+ # Using pluralization (Ten/Tens and Tenth/Tenths)
assert_equal "1 Tenth", number_to_human(0.1, locale: "ts")
assert_equal "1.3 Tenth", number_to_human(0.134, locale: "ts")
assert_equal "2 Tenths", number_to_human(0.2, locale: "ts")
@@ -144,7 +144,7 @@ module ActiveSupport
end
def test_number_to_human_with_custom_translation_scope
- #Significant was set to true with precision 2, with custom translated units
+ # Significant was set to true with precision 2, with custom translated units
assert_equal "4.3 cm", number_to_human(0.0432, locale: "ts", units: :custom_units_for_number_to_human)
end
end
diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb
index b4795b6ee1..16ccc5572c 100644
--- a/activesupport/test/number_helper_test.rb
+++ b/activesupport/test/number_helper_test.rb
@@ -260,7 +260,7 @@ module ActiveSupport
assert_equal "40 KB", number_helper.number_to_human_size(41100, precision: 2)
assert_equal "1.0 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 2, strip_insignificant_zeros: false)
assert_equal "1.012 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 3, significant: false)
- assert_equal "1 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 0, significant: true) #ignores significant it precision is 0
+ assert_equal "1 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 0, significant: true) # ignores significant it precision is 0
end
end
@@ -292,7 +292,7 @@ module ActiveSupport
assert_equal "489.0 Thousand", number_helper.number_to_human(489000, precision: 4, strip_insignificant_zeros: false)
assert_equal "1.2346 Million", number_helper.number_to_human(1234567, precision: 4, significant: false)
assert_equal "1,2 Million", number_helper.number_to_human(1234567, precision: 1, significant: false, separator: ",")
- assert_equal "1 Million", number_helper.number_to_human(1234567, precision: 0, significant: true, separator: ",") #significant forced to false
+ assert_equal "1 Million", number_helper.number_to_human(1234567, precision: 0, significant: true, separator: ",") # significant forced to false
assert_equal "1 Million", number_helper.number_to_human(999999)
assert_equal "1 Billion", number_helper.number_to_human(999999999)
end
@@ -300,13 +300,13 @@ module ActiveSupport
def test_number_to_human_with_custom_units
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- #Only integers
+ # Only integers
volume = { unit: "ml", thousand: "lt", million: "m3" }
assert_equal "123 lt", number_helper.number_to_human(123456, units: volume)
assert_equal "12 ml", number_helper.number_to_human(12, units: volume)
assert_equal "1.23 m3", number_helper.number_to_human(1234567, units: volume)
- #Including fractionals
+ # Including fractionals
distance = { mili: "mm", centi: "cm", deci: "dm", unit: "m", ten: "dam", hundred: "hm", thousand: "km" }
assert_equal "1.23 mm", number_helper.number_to_human(0.00123, units: distance)
assert_equal "1.23 cm", number_helper.number_to_human(0.0123, units: distance)
@@ -319,7 +319,7 @@ module ActiveSupport
assert_equal "1.23 km", number_helper.number_to_human(1230, units: distance)
assert_equal "12.3 km", number_helper.number_to_human(12300, units: distance)
- #The quantifiers don't need to be a continuous sequence
+ # The quantifiers don't need to be a continuous sequence
gangster = { hundred: "hundred bucks", million: "thousand quids" }
assert_equal "1 hundred bucks", number_helper.number_to_human(100, units: gangster)
assert_equal "25 hundred bucks", number_helper.number_to_human(2500, units: gangster)
@@ -329,11 +329,11 @@ module ActiveSupport
assert_equal "25 thousand quids", number_helper.number_to_human(25000000, units: gangster)
assert_equal "12300 thousand quids", number_helper.number_to_human(12345000000, units: gangster)
- #Spaces are stripped from the resulting string
+ # Spaces are stripped from the resulting string
assert_equal "4", number_helper.number_to_human(4, units: { unit: "", ten: "tens " })
assert_equal "4.5 tens", number_helper.number_to_human(45, units: { unit: "", ten: " tens " })
- #Uses only the provided units and does not try to use larger ones
+ # Uses only the provided units and does not try to use larger ones
assert_equal "1000 kilometers", number_helper.number_to_human(1_000_000, units: { unit: "meter", thousand: "kilometers" })
end
end
diff --git a/activesupport/test/ordered_options_test.rb b/activesupport/test/ordered_options_test.rb
index 7f2e774c02..90394fee0a 100644
--- a/activesupport/test/ordered_options_test.rb
+++ b/activesupport/test/ordered_options_test.rb
@@ -15,7 +15,7 @@ class OrderedOptionsTest < ActiveSupport::TestCase
a[:allow_concurrency] = false
assert_equal 1, a.size
- assert !a[:allow_concurrency]
+ assert_not a[:allow_concurrency]
a["else_where"] = 56
assert_equal 2, a.size
@@ -47,7 +47,7 @@ class OrderedOptionsTest < ActiveSupport::TestCase
a.allow_concurrency = false
assert_equal 1, a.size
- assert !a.allow_concurrency
+ assert_not a.allow_concurrency
a.else_where = 56
assert_equal 2, a.size
@@ -82,8 +82,8 @@ class OrderedOptionsTest < ActiveSupport::TestCase
def test_introspection
a = ActiveSupport::OrderedOptions.new
- assert a.respond_to?(:blah)
- assert a.respond_to?(:blah=)
+ assert_respond_to a, :blah
+ assert_respond_to a, :blah=
assert_equal 42, a.method(:blah=).call(42)
assert_equal 42, a.method(:blah).call
end
@@ -91,7 +91,20 @@ class OrderedOptionsTest < ActiveSupport::TestCase
def test_raises_with_bang
a = ActiveSupport::OrderedOptions.new
a[:foo] = :bar
- assert a.respond_to?(:foo!)
+ assert_respond_to a, :foo!
+
+ assert_nothing_raised { a.foo! }
+ assert_equal a.foo, a.foo!
+
+ assert_raises(KeyError) do
+ a.foo = nil
+ a.foo!
+ end
+ assert_raises(KeyError) { a.non_existing_key! }
+ end
+
+ def test_inheritable_options_with_bang
+ a = ActiveSupport::InheritableOptions.new(foo: :bar)
assert_nothing_raised { a.foo! }
assert_equal a.foo, a.foo!
diff --git a/activesupport/test/reloader_test.rb b/activesupport/test/reloader_test.rb
index 3e4229eaf7..976917c1a1 100644
--- a/activesupport/test/reloader_test.rb
+++ b/activesupport/test/reloader_test.rb
@@ -8,18 +8,18 @@ class ReloaderTest < ActiveSupport::TestCase
reloader.to_prepare { prepared = true }
reloader.to_complete { completed = true }
- assert !prepared
- assert !completed
+ assert_not prepared
+ assert_not completed
reloader.prepare!
assert prepared
- assert !completed
+ assert_not completed
prepared = false
reloader.wrap do
assert prepared
prepared = false
end
- assert !prepared
+ assert_not prepared
end
def test_prepend_prepare_callback
@@ -42,7 +42,7 @@ class ReloaderTest < ActiveSupport::TestCase
invoked = false
r.to_run { invoked = true }
r.wrap {}
- assert !invoked
+ assert_not invoked
end
def test_full_reload_sequence
diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb
index 05c2fb59be..1c19c92bb0 100644
--- a/activesupport/test/safe_buffer_test.rb
+++ b/activesupport/test/safe_buffer_test.rb
@@ -39,7 +39,7 @@ class SafeBufferTest < ActiveSupport::TestCase
end
test "Should be considered safe" do
- assert @buffer.html_safe?
+ assert_predicate @buffer, :html_safe?
end
test "Should return a safe buffer when calling to_s" do
@@ -78,13 +78,13 @@ class SafeBufferTest < ActiveSupport::TestCase
test "Should not return safe buffer from gsub" do
altered_buffer = @buffer.gsub("", "asdf")
assert_equal "asdf", altered_buffer
- assert !altered_buffer.html_safe?
+ assert_not_predicate altered_buffer, :html_safe?
end
test "Should not return safe buffer from gsub!" do
@buffer.gsub!("", "asdf")
assert_equal "asdf", @buffer
- assert !@buffer.html_safe?
+ assert_not_predicate @buffer, :html_safe?
end
test "Should escape dirty buffers on add" do
@@ -101,13 +101,13 @@ class SafeBufferTest < ActiveSupport::TestCase
test "Should preserve html_safe? status on copy" do
@buffer.gsub!("", "<>")
- assert !@buffer.dup.html_safe?
+ assert_not_predicate @buffer.dup, :html_safe?
end
test "Should return safe buffer when added with another safe buffer" do
clean = "<script>".html_safe
result_buffer = @buffer + clean
- assert result_buffer.html_safe?
+ assert_predicate result_buffer, :html_safe?
assert_equal "<script>", result_buffer
end
@@ -127,8 +127,8 @@ class SafeBufferTest < ActiveSupport::TestCase
end
test "clone_empty keeps the original dirtyness" do
- assert @buffer.clone_empty.html_safe?
- assert !@buffer.gsub!("", "").clone_empty.html_safe?
+ assert_predicate @buffer.clone_empty, :html_safe?
+ assert_not_predicate @buffer.gsub!("", "").clone_empty, :html_safe?
end
test "Should be safe when sliced if original value was safe" do
diff --git a/activesupport/test/security_utils_test.rb b/activesupport/test/security_utils_test.rb
index efd2bcfa0f..fff9cc2a8d 100644
--- a/activesupport/test/security_utils_test.rb
+++ b/activesupport/test/security_utils_test.rb
@@ -9,8 +9,14 @@ class SecurityUtilsTest < ActiveSupport::TestCase
assert_not ActiveSupport::SecurityUtils.secure_compare("a", "b")
end
- def test_variable_size_secure_compare_should_perform_string_comparison
- assert ActiveSupport::SecurityUtils.variable_size_secure_compare("a", "a")
- assert_not ActiveSupport::SecurityUtils.variable_size_secure_compare("a", "b")
+ def test_fixed_length_secure_compare_should_perform_string_comparison
+ assert ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "a")
+ assert_not ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "b")
+ end
+
+ def test_fixed_length_secure_compare_raise_on_length_mismatch
+ assert_raises(ArgumentError, "string length mismatch.") do
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "ab")
+ end
end
end
diff --git a/activesupport/test/string_inquirer_test.rb b/activesupport/test/string_inquirer_test.rb
index bf8f878a32..09726b144a 100644
--- a/activesupport/test/string_inquirer_test.rb
+++ b/activesupport/test/string_inquirer_test.rb
@@ -8,11 +8,11 @@ class StringInquirerTest < ActiveSupport::TestCase
end
def test_match
- assert @string_inquirer.production?
+ assert_predicate @string_inquirer, :production?
end
def test_miss
- assert_not @string_inquirer.development?
+ assert_not_predicate @string_inquirer, :development?
end
def test_missing_question_mark
diff --git a/activesupport/test/tagged_logging_test.rb b/activesupport/test/tagged_logging_test.rb
index 5fd6a6b316..e2b41cf8ee 100644
--- a/activesupport/test/tagged_logging_test.rb
+++ b/activesupport/test/tagged_logging_test.rb
@@ -21,7 +21,7 @@ class TaggedLoggingTest < ActiveSupport::TestCase
assert_nil logger.formatter
ActiveSupport::TaggedLogging.new(logger)
assert_not_nil logger.formatter
- assert logger.formatter.respond_to?(:tagged)
+ assert_respond_to logger.formatter, :tagged
end
test "tagged once" do
@@ -74,11 +74,12 @@ class TaggedLoggingTest < ActiveSupport::TestCase
test "keeps each tag in their own thread" do
@logger.tagged("BCX") do
Thread.new do
+ @logger.info "Dull story"
@logger.tagged("OMG") { @logger.info "Cool story" }
end.join
@logger.info "Funky time"
end
- assert_equal "[OMG] Cool story\n[BCX] Funky time\n", @output.string
+ assert_equal "Dull story\n[OMG] Cool story\n[BCX] Funky time\n", @output.string
end
test "keeps each tag in their own instance" do
diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb
index 65b1cb4a14..8a1ecb6b33 100644
--- a/activesupport/test/test_case_test.rb
+++ b/activesupport/test/test_case_test.rb
@@ -2,7 +2,7 @@
require "abstract_unit"
-class AssertDifferenceTest < ActiveSupport::TestCase
+class AssertionsTest < ActiveSupport::TestCase
def setup
@object = Class.new do
attr_accessor :num
@@ -87,7 +87,7 @@ class AssertDifferenceTest < ActiveSupport::TestCase
def test_expression_is_evaluated_in_the_appropriate_scope
silence_warnings do
- local_scope = "foo";
+ local_scope = "foo"
local_scope = local_scope # to suppress unused variable warning
assert_difference("local_scope; @object.num") { @object.increment }
end
@@ -115,6 +115,35 @@ class AssertDifferenceTest < ActiveSupport::TestCase
end
end
+ def test_hash_of_expressions
+ assert_difference "@object.num" => 1, "@object.num + 1" => 1 do
+ @object.increment
+ end
+ end
+
+ def test_hash_of_expressions_with_message
+ error = assert_raises Minitest::Assertion do
+ assert_difference({ "@object.num" => 0 }, "Object Changed") do
+ @object.increment
+ end
+ end
+ assert_equal "Object Changed.\n\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message
+ end
+
+ def test_hash_of_lambda_expressions
+ assert_difference -> { @object.num } => 1, -> { @object.num + 1 } => 1 do
+ @object.increment
+ end
+ end
+
+ def test_hash_of_expressions_identify_failure
+ assert_raises(Minitest::Assertion) do
+ assert_difference "@object.num" => 1, "1 + 1" => 1 do
+ @object.increment
+ end
+ end
+ end
+
def test_assert_changes_pass
assert_changes "@object.num" do
@object.increment
@@ -156,6 +185,16 @@ class AssertDifferenceTest < ActiveSupport::TestCase
end
end
+ def test_assert_changes_with_to_option_but_no_change_has_special_message
+ error = assert_raises Minitest::Assertion do
+ assert_changes "@object.num", to: 0 do
+ # no changes
+ end
+ end
+
+ assert_equal "\"@object.num\" didn't change. It was already 0", error.message
+ end
+
def test_assert_changes_with_wrong_to_option
assert_raises Minitest::Assertion do
assert_changes "@object.num", to: 2 do
@@ -179,6 +218,7 @@ class AssertDifferenceTest < ActiveSupport::TestCase
end
def test_assert_changes_works_with_any_object
+ # Silences: instance variable @new_object not initialized.
retval = silence_warnings do
assert_changes :@new_object, from: nil, to: 42 do
@new_object = 42
@@ -201,7 +241,7 @@ class AssertDifferenceTest < ActiveSupport::TestCase
def test_assert_changes_with_to_and_case_operator
token = nil
- assert_changes -> { token }, to: /\w{32}/ do
+ assert_changes -> { token }, to: /\w{32}/ do
token = SecureRandom.hex
end
end
@@ -217,6 +257,7 @@ class AssertDifferenceTest < ActiveSupport::TestCase
def test_assert_changes_with_message
error = assert_raises Minitest::Assertion do
assert_changes "@object.num", "@object.num should 1", to: 1 do
+ @object.decrement
end
end
@@ -236,7 +277,7 @@ class AssertDifferenceTest < ActiveSupport::TestCase
end
end
- assert_equal "@object.num should not change.\n\"@object.num\" did change to 1.\nExpected: 0\n Actual: 1", error.message
+ assert_equal "@object.num should not change.\n\"@object.num\" did change to 1", error.message
end
end
diff --git a/activesupport/test/testing/after_teardown_test.rb b/activesupport/test/testing/after_teardown_test.rb
new file mode 100644
index 0000000000..961af49479
--- /dev/null
+++ b/activesupport/test/testing/after_teardown_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module OtherAfterTeardown
+ def after_teardown
+ super
+
+ @witness = true
+ end
+end
+
+class AfterTeardownTest < ActiveSupport::TestCase
+ include OtherAfterTeardown
+
+ attr_writer :witness
+
+ MyError = Class.new(StandardError)
+
+ teardown do
+ raise MyError, "Test raises an error, all after_teardown should still get called"
+ end
+
+ def after_teardown
+ assert_changes -> { failures.count }, from: 0, to: 1 do
+ super
+ end
+
+ assert_equal true, @witness
+ failures.clear
+ end
+
+ def test_teardown_raise_but_all_after_teardown_method_are_called
+ assert true
+ end
+end
diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb
index 4172c18ead..9c2c635f43 100644
--- a/activesupport/test/time_travel_test.rb
+++ b/activesupport/test/time_travel_test.rb
@@ -98,7 +98,7 @@ class TimeTravelTest < ActiveSupport::TestCase
travel_to outer_expected_time do
e = assert_raises(RuntimeError) do
travel_to(inner_expected_time) do
- #noop
+ # noop
end
end
assert_match(/Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing\./, e.message)
diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb
index acb0ecd226..b59f3e9405 100644
--- a/activesupport/test/time_zone_test.rb
+++ b/activesupport/test/time_zone_test.rb
@@ -32,6 +32,12 @@ class TimeZoneTest < ActiveSupport::TestCase
end
end
+ def test_period_for_local_with_ambigiuous_time
+ zone = ActiveSupport::TimeZone["Moscow"]
+ period = zone.period_for_local(Time.utc(2015, 1, 1))
+ assert_equal period, zone.period_for_local(Time.utc(2014, 10, 26, 1, 0, 0))
+ end
+
def test_from_integer_to_map
assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone[-28800] # PST
end
@@ -195,6 +201,11 @@ class TimeZoneTest < ActiveSupport::TestCase
assert_equal "EDT", twz.zone
end
+ def test_local_with_ambiguous_time
+ zone = ActiveSupport::TimeZone["Moscow"]
+ assert_equal Time.utc(2014, 10, 25, 22, 0, 0), zone.local(2014, 10, 26, 1, 0, 0)
+ end
+
def test_at
zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
secs = 946684800.0
@@ -303,6 +314,11 @@ class TimeZoneTest < ActiveSupport::TestCase
end
end
+ def test_iso8601_with_ambiguous_time
+ zone = ActiveSupport::TimeZone["Moscow"]
+ assert_equal Time.utc(2014, 10, 25, 22, 0, 0), zone.parse("2014-10-26T01:00:00")
+ end
+
def test_parse
zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
twz = zone.parse("1999-12-31 19:00:00")
@@ -412,6 +428,11 @@ class TimeZoneTest < ActiveSupport::TestCase
assert_equal "argument out of range", exception.message
end
+ def test_parse_with_ambiguous_time
+ zone = ActiveSupport::TimeZone["Moscow"]
+ assert_equal Time.utc(2014, 10, 25, 22, 0, 0), zone.parse("2014-10-26 01:00:00")
+ end
+
def test_rfc3339
zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
twz = zone.rfc3339("1999-12-31T14:00:00-10:00")
@@ -604,6 +625,11 @@ class TimeZoneTest < ActiveSupport::TestCase
end
end
+ def test_strptime_with_ambiguous_time
+ zone = ActiveSupport::TimeZone["Moscow"]
+ assert_equal Time.utc(2014, 10, 25, 22, 0, 0), zone.strptime("2014-10-26 01:00:00", "%Y-%m-%d %H:%M:%S")
+ end
+
def test_utc_offset_lazy_loaded_from_tzinfo_when_not_passed_in_to_initialize
tzinfo = TZInfo::Timezone.get("America/New_York")
zone = ActiveSupport::TimeZone.create(tzinfo.name, nil, tzinfo)
@@ -692,6 +718,28 @@ class TimeZoneTest < ActiveSupport::TestCase
end
end
+ def test_all_uninfluenced_by_time_zone_lookups_delegated_to_tzinfo
+ ActiveSupport::TimeZone.clear
+ galapagos = ActiveSupport::TimeZone["Pacific/Galapagos"]
+ all_zones = ActiveSupport::TimeZone.all
+ assert_not_includes all_zones, galapagos
+ end
+
+ def test_all_doesnt_raise_exception_with_missing_tzinfo_data
+ mappings = {
+ "Puerto Rico" => "America/Unknown",
+ "Pittsburgh" => "America/New_York"
+ }
+
+ with_tz_mappings(mappings) do
+ assert_nil ActiveSupport::TimeZone["Puerto Rico"]
+ assert_nil ActiveSupport::TimeZone[-9]
+ assert_nothing_raised do
+ ActiveSupport::TimeZone.all
+ end
+ end
+ end
+
def test_index
assert_nil ActiveSupport::TimeZone["bogus"]
assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone["Central Time (US & Canada)"]
@@ -723,6 +771,16 @@ class TimeZoneTest < ActiveSupport::TestCase
assert_not_includes ActiveSupport::TimeZone.country_zones(:ru), ActiveSupport::TimeZone["Kuala Lumpur"]
end
+ def test_country_zones_with_and_without_mappings
+ assert_includes ActiveSupport::TimeZone.country_zones("au"), ActiveSupport::TimeZone["Adelaide"]
+ assert_includes ActiveSupport::TimeZone.country_zones("au"), ActiveSupport::TimeZone["Australia/Lord_Howe"]
+ end
+
+ def test_country_zones_with_multiple_mappings
+ assert_includes ActiveSupport::TimeZone.country_zones("gb"), ActiveSupport::TimeZone["Edinburgh"]
+ assert_includes ActiveSupport::TimeZone.country_zones("gb"), ActiveSupport::TimeZone["London"]
+ end
+
def test_country_zones_without_mappings
assert_includes ActiveSupport::TimeZone.country_zones(:sv), ActiveSupport::TimeZone["America/El_Salvador"]
end
diff --git a/activesupport/test/time_zone_test_helpers.rb b/activesupport/test/time_zone_test_helpers.rb
index 051703a781..85ed727c9b 100644
--- a/activesupport/test/time_zone_test_helpers.rb
+++ b/activesupport/test/time_zone_test_helpers.rb
@@ -23,4 +23,17 @@ module TimeZoneTestHelpers
ensure
ActiveSupport.to_time_preserves_timezone = old_preserve_tz
end
+
+ def with_tz_mappings(mappings)
+ old_mappings = ActiveSupport::TimeZone::MAPPING.dup
+ ActiveSupport::TimeZone.clear
+ ActiveSupport::TimeZone::MAPPING.clear
+ ActiveSupport::TimeZone::MAPPING.merge!(mappings)
+
+ yield
+ ensure
+ ActiveSupport::TimeZone.clear
+ ActiveSupport::TimeZone::MAPPING.clear
+ ActiveSupport::TimeZone::MAPPING.merge!(old_mappings)
+ end
end
diff --git a/activesupport/test/xml_mini/libxml_engine_test.rb b/activesupport/test/xml_mini/libxml_engine_test.rb
index 5c13f493f1..3eef3946a3 100644
--- a/activesupport/test/xml_mini/libxml_engine_test.rb
+++ b/activesupport/test/xml_mini/libxml_engine_test.rb
@@ -6,7 +6,7 @@ XMLMiniEngineTest.run_with_gem("libxml") do
class LibxmlEngineTest < XMLMiniEngineTest
def setup
super
- LibXML::XML::Error.set_handler(&lambda { |error| }) #silence libxml, exceptions will do
+ LibXML::XML::Error.set_handler(&lambda { |error| }) # silence libxml, exceptions will do
end
private
diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb
index 5e46406a7b..18a3f2ca66 100644
--- a/activesupport/test/xml_mini_test.rb
+++ b/activesupport/test/xml_mini_test.rb
@@ -114,7 +114,7 @@ module XmlMiniTest
end
test "#to_tag accepts decimal types" do
- @xml.to_tag(:b, ::BigDecimal.new("1.2"), @options)
+ @xml.to_tag(:b, BigDecimal("1.2"), @options)
assert_xml("<b type=\"decimal\">1.2</b>")
end
diff --git a/ci/custom_cops/bin/test b/ci/custom_cops/bin/test
new file mode 100755
index 0000000000..495ffec83a
--- /dev/null
+++ b/ci/custom_cops/bin/test
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require_relative "../../../tools/test"
diff --git a/ci/custom_cops/lib/custom_cops.rb b/ci/custom_cops/lib/custom_cops.rb
new file mode 100644
index 0000000000..157b8247e4
--- /dev/null
+++ b/ci/custom_cops/lib/custom_cops.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+require_relative "custom_cops/refute_not"
+require_relative "custom_cops/assert_not"
diff --git a/ci/custom_cops/lib/custom_cops/assert_not.rb b/ci/custom_cops/lib/custom_cops/assert_not.rb
new file mode 100644
index 0000000000..e722448e21
--- /dev/null
+++ b/ci/custom_cops/lib/custom_cops/assert_not.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module CustomCops
+ # Enforces the use of `assert_not` over `assert !`.
+ #
+ # @example
+ # # bad
+ # assert !x
+ # assert ! x
+ #
+ # # good
+ # assert_not x
+ #
+ class AssertNot < RuboCop::Cop::Cop
+ MSG = "Prefer `assert_not` over `assert !`"
+
+ def_node_matcher :offensive?, "(send nil? :assert (send ... :!))"
+
+ def on_send(node)
+ add_offense(node) if offensive?(node)
+ end
+
+ def autocorrect(node)
+ expression = node.loc.expression
+
+ ->(corrector) do
+ corrector.replace(
+ expression,
+ corrected_source(expression.source)
+ )
+ end
+ end
+
+ private
+
+ def corrected_source(source)
+ source.gsub(/^assert(\(| ) *! */, "assert_not\\1")
+ end
+ end
+end
diff --git a/ci/custom_cops/lib/custom_cops/refute_not.rb b/ci/custom_cops/lib/custom_cops/refute_not.rb
new file mode 100644
index 0000000000..3e89e0fd32
--- /dev/null
+++ b/ci/custom_cops/lib/custom_cops/refute_not.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module CustomCops
+ # Enforces the use of `#assert_not` methods over `#refute` methods.
+ #
+ # @example
+ # # bad
+ # refute false
+ # refute_empty [1, 2, 3]
+ # refute_equal true, false
+ #
+ # # good
+ # assert_not false
+ # assert_not_empty [1, 2, 3]
+ # assert_not_equal true, false
+ #
+ class RefuteNot < RuboCop::Cop::Cop
+ MSG = "Prefer `%<assert_method>s` over `%<refute_method>s`"
+
+ CORRECTIONS = {
+ refute: "assert_not",
+ refute_empty: "assert_not_empty",
+ refute_equal: "assert_not_equal",
+ refute_in_delta: "assert_not_in_delta",
+ refute_in_epsilon: "assert_not_in_epsilon",
+ refute_includes: "assert_not_includes",
+ refute_instance_of: "assert_not_instance_of",
+ refute_kind_of: "assert_not_kind_of",
+ refute_nil: "assert_not_nil",
+ refute_operator: "assert_not_operator",
+ refute_predicate: "assert_not_predicate",
+ refute_respond_to: "assert_not_respond_to",
+ refute_same: "assert_not_same",
+ refute_match: "assert_no_match"
+ }.freeze
+
+ OFFENSIVE_METHODS = CORRECTIONS.keys.freeze
+
+ def_node_matcher :offensive?, "(send nil? #offensive_method? ...)"
+
+ def on_send(node)
+ return unless offensive?(node)
+
+ message = offense_message(node.method_name)
+ add_offense(node, location: :selector, message: message)
+ end
+
+ def autocorrect(node)
+ ->(corrector) do
+ corrector.replace(
+ node.loc.selector,
+ CORRECTIONS[node.method_name]
+ )
+ end
+ end
+
+ private
+
+ def offensive_method?(method_name)
+ OFFENSIVE_METHODS.include?(method_name)
+ end
+
+ def offense_message(method_name)
+ format(
+ MSG,
+ refute_method: method_name,
+ assert_method: CORRECTIONS[method_name]
+ )
+ end
+ end
+end
diff --git a/ci/custom_cops/test/custom_cops/assert_not_test.rb b/ci/custom_cops/test/custom_cops/assert_not_test.rb
new file mode 100644
index 0000000000..abb151aeb4
--- /dev/null
+++ b/ci/custom_cops/test/custom_cops/assert_not_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "support/cop_helper"
+require_relative "../../lib/custom_cops/assert_not"
+
+class AssertNotTest < ActiveSupport::TestCase
+ include CopHelper
+
+ setup do
+ @cop = CustomCops::AssertNot.new
+ end
+
+ test "rejects 'assert !'" do
+ inspect_source @cop, "assert !x"
+ assert_offense @cop, "^^^^^^^^^ Prefer `assert_not` over `assert !`"
+ end
+
+ test "rejects 'assert !' with a complex value" do
+ inspect_source @cop, "assert !a.b(c)"
+ assert_offense @cop, "^^^^^^^^^^^^^^ Prefer `assert_not` over `assert !`"
+ end
+
+ test "autocorrects `assert !`" do
+ corrected = autocorrect_source(@cop, "assert !false")
+ assert_equal "assert_not false", corrected
+ end
+
+ test "autocorrects `assert !` with extra spaces" do
+ corrected = autocorrect_source(@cop, "assert ! false")
+ assert_equal "assert_not false", corrected
+ end
+
+ test "autocorrects `assert !` with parentheses" do
+ corrected = autocorrect_source(@cop, "assert(!false)")
+ assert_equal "assert_not(false)", corrected
+ end
+
+ test "accepts `assert_not`" do
+ inspect_source @cop, "assert_not x"
+ assert_empty @cop.offenses
+ end
+end
diff --git a/ci/custom_cops/test/custom_cops/refute_not_test.rb b/ci/custom_cops/test/custom_cops/refute_not_test.rb
new file mode 100644
index 0000000000..f0f6eaeda0
--- /dev/null
+++ b/ci/custom_cops/test/custom_cops/refute_not_test.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require "support/cop_helper"
+require_relative "../../lib/custom_cops/refute_not"
+
+class RefuteNotTest < ActiveSupport::TestCase
+ include CopHelper
+
+ setup do
+ @cop = CustomCops::RefuteNot.new
+ end
+
+ {
+ refute: :assert_not,
+ refute_empty: :assert_not_empty,
+ refute_equal: :assert_not_equal,
+ refute_in_delta: :assert_not_in_delta,
+ refute_in_epsilon: :assert_not_in_epsilon,
+ refute_includes: :assert_not_includes,
+ refute_instance_of: :assert_not_instance_of,
+ refute_kind_of: :assert_not_kind_of,
+ refute_nil: :assert_not_nil,
+ refute_operator: :assert_not_operator,
+ refute_predicate: :assert_not_predicate,
+ refute_respond_to: :assert_not_respond_to,
+ refute_same: :assert_not_same,
+ refute_match: :assert_no_match
+ }.each do |refute_method, assert_method|
+ test "rejects `#{refute_method}` with a single argument" do
+ inspect_source(@cop, "#{refute_method} a")
+ assert_offense @cop, offense_message(refute_method, assert_method)
+ end
+
+ test "rejects `#{refute_method}` with multiple arguments" do
+ inspect_source(@cop, "#{refute_method} a, b, c")
+ assert_offense @cop, offense_message(refute_method, assert_method)
+ end
+
+ test "autocorrects `#{refute_method}` with a single argument" do
+ corrected = autocorrect_source(@cop, "#{refute_method} a")
+ assert_equal "#{assert_method} a", corrected
+ end
+
+ test "autocorrects `#{refute_method}` with multiple arguments" do
+ corrected = autocorrect_source(@cop, "#{refute_method} a, b, c")
+ assert_equal "#{assert_method} a, b, c", corrected
+ end
+
+ test "accepts `#{assert_method}` with a single argument" do
+ inspect_source(@cop, "#{assert_method} a")
+ assert_empty @cop.offenses
+ end
+
+ test "accepts `#{assert_method}` with multiple arguments" do
+ inspect_source(@cop, "#{assert_method} a, b, c")
+ assert_empty @cop.offenses
+ end
+ end
+
+ private
+
+ def offense_message(refute_method, assert_method)
+ carets = "^" * refute_method.to_s.length
+ "#{carets} Prefer `#{assert_method}` over `#{refute_method}`"
+ end
+end
diff --git a/ci/custom_cops/test/support/cop_helper.rb b/ci/custom_cops/test/support/cop_helper.rb
new file mode 100644
index 0000000000..c2c6b969dd
--- /dev/null
+++ b/ci/custom_cops/test/support/cop_helper.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "rubocop"
+
+module CopHelper
+ def inspect_source(cop, source)
+ processed_source = parse_source(source)
+ raise "Error parsing example code" unless processed_source.valid_syntax?
+ investigate(cop, processed_source)
+ processed_source
+ end
+
+ def autocorrect_source(cop, source)
+ cop.instance_variable_get(:@options)[:auto_correct] = true
+ processed_source = inspect_source(cop, source)
+ rewrite(cop, processed_source)
+ end
+
+ def assert_offense(cop, expected_message)
+ assert_not_empty(
+ cop.offenses,
+ "Expected offense with message \"#{expected_message}\", but got no offense"
+ )
+
+ offense = cop.offenses.first
+ carets = "^" * offense.column_length
+
+ assert_equal expected_message, "#{carets} #{offense.message}"
+ end
+
+ private
+ TARGET_RUBY_VERSION = 2.4
+
+ def parse_source(source)
+ RuboCop::ProcessedSource.new(source, TARGET_RUBY_VERSION)
+ end
+
+ def rewrite(cop, processed_source)
+ RuboCop::Cop::Corrector.new(processed_source.buffer, cop.corrections)
+ .rewrite
+ end
+
+ def investigate(cop, processed_source)
+ RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
+ .investigate(processed_source)
+ end
+end
diff --git a/ci/phantomjs.js b/ci/phantomjs.js
deleted file mode 100644
index 7a33fb14a3..0000000000
--- a/ci/phantomjs.js
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * PhantomJS Runner QUnit Plugin 1.2.0
- *
- * PhantomJS binaries: http://phantomjs.org/download.html
- * Requires PhantomJS 1.6+ (1.7+ recommended)
- *
- * Run with:
- * phantomjs runner.js [url-of-your-qunit-testsuite]
- *
- * e.g.
- * phantomjs runner.js http://localhost/qunit/test/index.html
- */
-
-/*global phantom:false, require:false, console:false, window:false, QUnit:false */
-
-(function() {
- 'use strict';
-
- var url, page, timeout,
- args = require('system').args;
-
- // arg[0]: scriptName, args[1...]: arguments
- if (args.length < 2 || args.length > 3) {
- console.error('Usage:\n phantomjs runner.js [url-of-your-qunit-testsuite] [timeout-in-seconds]');
- phantom.exit(1);
- }
-
- url = args[1];
- page = require('webpage').create();
- if (args[2] !== undefined) {
- timeout = parseInt(args[2], 10);
- }
-
- // Route `console.log()` calls from within the Page context to the main Phantom context (i.e. current `this`)
- page.onConsoleMessage = function(msg) {
- console.log(msg);
- };
-
- page.onInitialized = function() {
- page.evaluate(addLogging);
- };
-
- page.onCallback = function(message) {
- var result,
- failed;
-
- if (message) {
- if (message.name === 'QUnit.done') {
- result = message.data;
- failed = !result || !result.total || result.failed;
-
- if (!result.total) {
- console.error('No tests were executed. Are you loading tests asynchronously?');
- }
-
- phantom.exit(failed ? 1 : 0);
- }
- }
- };
-
- page.open(url, function(status) {
- if (status !== 'success') {
- console.error('Unable to access network: ' + status);
- phantom.exit(1);
- } else {
- // Cannot do this verification with the 'DOMContentLoaded' handler because it
- // will be too late to attach it if a page does not have any script tags.
- var qunitMissing = page.evaluate(function() { return (typeof QUnit === 'undefined' || !QUnit); });
- if (qunitMissing) {
- console.error('The `QUnit` object is not present on this page.');
- phantom.exit(1);
- }
-
- // Set a timeout on the test running, otherwise tests with async problems will hang forever
- if (typeof timeout === 'number') {
- setTimeout(function() {
- console.error('The specified timeout of ' + timeout + ' seconds has expired. Aborting...');
- phantom.exit(1);
- }, timeout * 1000);
- }
-
- // Do nothing... the callback mechanism will handle everything!
- }
- });
-
- function addLogging() {
- window.document.addEventListener('DOMContentLoaded', function() {
- var currentTestAssertions = [];
-
- QUnit.log(function(details) {
- var response;
-
- // Ignore passing assertions
- if (details.result) {
- return;
- }
-
- response = details.message || '';
-
- if (typeof details.expected !== 'undefined') {
- if (response) {
- response += ', ';
- }
-
- response += 'expected: ' + details.expected + ', but was: ' + details.actual;
- }
-
- if (details.source) {
- response += "\n" + details.source;
- }
-
- currentTestAssertions.push('Failed assertion: ' + response);
- });
-
- QUnit.testDone(function(result) {
- var i,
- len,
- name = '';
-
- if (result.module) {
- name += result.module + ': ';
- }
- name += result.name;
-
- if (result.failed) {
- console.log('\n' + 'Test failed: ' + name);
-
- for (i = 0, len = currentTestAssertions.length; i < len; i++) {
- console.log(' ' + currentTestAssertions[i]);
- }
- }
-
- currentTestAssertions.length = 0;
- });
-
- QUnit.done(function(result) {
- console.log('\n' + 'Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.');
-
- if (typeof window.callPhantom === 'function') {
- window.callPhantom({
- 'name': 'QUnit.done',
- 'data': result
- });
- }
- });
- }, false);
- }
-})();
-
diff --git a/ci/qunit-selenium-runner.rb b/ci/qunit-selenium-runner.rb
new file mode 100644
index 0000000000..05bcab8cdb
--- /dev/null
+++ b/ci/qunit-selenium-runner.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require "qunit/selenium/test_runner"
+require "chromedriver/helper"
+
+driver_options = Selenium::WebDriver::Chrome::Options.new
+driver_options.add_argument("--headless")
+driver_options.add_argument("--disable-gpu")
+driver_options.add_argument("--no-sandbox")
+
+driver = ::Selenium::WebDriver.for(:chrome, options: driver_options)
+result = QUnit::Selenium::TestRunner.new(driver).open(ARGV[0], timeout: 60)
+driver.quit
+
+puts "Time: #{result.duration} seconds, Total: #{result.assertions[:total]}, Passed: #{result.assertions[:passed]}, Failed: #{result.assertions[:failed]}"
+exit(result.tests[:failed] > 0 ? 1 : 0)
diff --git a/ci/travis.rb b/ci/travis.rb
index b124358789..861063afa5 100755
--- a/ci/travis.rb
+++ b/ci/travis.rb
@@ -135,7 +135,7 @@ class Build
if activesupport? && !isolated?
# There is a known issue with the listen tests that causes files to be
# incorrectly GC'ed even when they are still in-use. The current solution
- # is to only run them in isolation to avoid randomly failing our test suite.
+ # is to only run them in isolation to avoid random failures of our test suite.
{ "LISTEN" => "0" }
else
{}
@@ -159,7 +159,7 @@ results = {}
ENV["GEM"].split(",").each do |gem|
[false, true].each do |isolated|
next if ENV["TRAVIS_PULL_REQUEST"] && ENV["TRAVIS_PULL_REQUEST"] != "false" && isolated
- next if RUBY_VERSION < "2.4" && isolated
+ next if RUBY_VERSION < "2.5" && isolated
next if gem == "railties" && isolated
next if gem == "ac" && isolated
next if gem == "ac:integration" && isolated
diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md
index d8b122d264..0307e06fd9 100644
--- a/guides/CHANGELOG.md
+++ b/guides/CHANGELOG.md
@@ -1 +1,6 @@
-Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/guides/CHANGELOG.md) for previous changes.
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
+
+
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/guides/CHANGELOG.md) for previous changes.
diff --git a/guides/assets/images/rails4_features.png b/guides/assets/images/4_0_release_notes/rails4_features.png
index ac73f05cf7..ac73f05cf7 100644
--- a/guides/assets/images/rails4_features.png
+++ b/guides/assets/images/4_0_release_notes/rails4_features.png
Binary files differ
diff --git a/guides/assets/images/akshaysurve.jpg b/guides/assets/images/akshaysurve.jpg
deleted file mode 100644
index cfc3333958..0000000000
--- a/guides/assets/images/akshaysurve.jpg
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/association_basics/belongs_to.png b/guides/assets/images/association_basics/belongs_to.png
new file mode 100644
index 0000000000..2b8c1d52ea
--- /dev/null
+++ b/guides/assets/images/association_basics/belongs_to.png
Binary files differ
diff --git a/guides/assets/images/association_basics/habtm.png b/guides/assets/images/association_basics/habtm.png
new file mode 100644
index 0000000000..7e508cc1a6
--- /dev/null
+++ b/guides/assets/images/association_basics/habtm.png
Binary files differ
diff --git a/guides/assets/images/association_basics/has_many.png b/guides/assets/images/association_basics/has_many.png
new file mode 100644
index 0000000000..36ccf9f0f6
--- /dev/null
+++ b/guides/assets/images/association_basics/has_many.png
Binary files differ
diff --git a/guides/assets/images/association_basics/has_many_through.png b/guides/assets/images/association_basics/has_many_through.png
new file mode 100644
index 0000000000..9e9caabd73
--- /dev/null
+++ b/guides/assets/images/association_basics/has_many_through.png
Binary files differ
diff --git a/guides/assets/images/association_basics/has_one.png b/guides/assets/images/association_basics/has_one.png
new file mode 100644
index 0000000000..c29c6b9c59
--- /dev/null
+++ b/guides/assets/images/association_basics/has_one.png
Binary files differ
diff --git a/guides/assets/images/association_basics/has_one_through.png b/guides/assets/images/association_basics/has_one_through.png
new file mode 100644
index 0000000000..fdf13286c4
--- /dev/null
+++ b/guides/assets/images/association_basics/has_one_through.png
Binary files differ
diff --git a/guides/assets/images/association_basics/polymorphic.png b/guides/assets/images/association_basics/polymorphic.png
new file mode 100644
index 0000000000..d630db9e01
--- /dev/null
+++ b/guides/assets/images/association_basics/polymorphic.png
Binary files differ
diff --git a/guides/assets/images/belongs_to.png b/guides/assets/images/belongs_to.png
deleted file mode 100644
index 1a9926e578..0000000000
--- a/guides/assets/images/belongs_to.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/credits_pic_blank.gif b/guides/assets/images/credits_pic_blank.gif
deleted file mode 100644
index a6b335d0c9..0000000000
--- a/guides/assets/images/credits_pic_blank.gif
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/fxn.png b/guides/assets/images/fxn.png
deleted file mode 100644
index 733d380cba..0000000000
--- a/guides/assets/images/fxn.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/rails_welcome.png b/guides/assets/images/getting_started/rails_welcome.png
index 44f89ec8de..88efe34a9d 100644
--- a/guides/assets/images/getting_started/rails_welcome.png
+++ b/guides/assets/images/getting_started/rails_welcome.png
Binary files differ
diff --git a/guides/assets/images/getting_started/routing_error_no_route_matches.png b/guides/assets/images/getting_started/routing_error_no_route_matches.png
deleted file mode 100644
index 08c54f921f..0000000000
--- a/guides/assets/images/getting_started/routing_error_no_route_matches.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/habtm.png b/guides/assets/images/habtm.png
deleted file mode 100644
index 41013b743d..0000000000
--- a/guides/assets/images/habtm.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/has_many.png b/guides/assets/images/has_many.png
deleted file mode 100644
index 0d67bea38b..0000000000
--- a/guides/assets/images/has_many.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/has_many_through.png b/guides/assets/images/has_many_through.png
deleted file mode 100644
index b4da60e1fb..0000000000
--- a/guides/assets/images/has_many_through.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/has_one.png b/guides/assets/images/has_one.png
deleted file mode 100644
index c70763856a..0000000000
--- a/guides/assets/images/has_one.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/has_one_through.png b/guides/assets/images/has_one_through.png
deleted file mode 100644
index 888a02b775..0000000000
--- a/guides/assets/images/has_one_through.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/header_backdrop.png b/guides/assets/images/header_backdrop.png
deleted file mode 100644
index 81f4d91774..0000000000
--- a/guides/assets/images/header_backdrop.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/README b/guides/assets/images/icons/README
deleted file mode 100644
index 09da77fc86..0000000000
--- a/guides/assets/images/icons/README
+++ /dev/null
@@ -1,5 +0,0 @@
-Replaced the plain DocBook XSL admonition icons with Jimmac's DocBook
-icons (http://jimmac.musichall.cz/ikony.php3). I dropped transparency
-from the Jimmac icons to get round MS IE and FOP PNG incompatibilities.
-
-Stuart Rackham
diff --git a/guides/assets/images/icons/callouts/1.png b/guides/assets/images/icons/callouts/1.png
deleted file mode 100644
index c5d02adcf4..0000000000
--- a/guides/assets/images/icons/callouts/1.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/10.png b/guides/assets/images/icons/callouts/10.png
deleted file mode 100644
index fe89f9ef83..0000000000
--- a/guides/assets/images/icons/callouts/10.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/11.png b/guides/assets/images/icons/callouts/11.png
deleted file mode 100644
index 3b7b9318e7..0000000000
--- a/guides/assets/images/icons/callouts/11.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/12.png b/guides/assets/images/icons/callouts/12.png
deleted file mode 100644
index 7b95925e9d..0000000000
--- a/guides/assets/images/icons/callouts/12.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/13.png b/guides/assets/images/icons/callouts/13.png
deleted file mode 100644
index 4b99fe8efc..0000000000
--- a/guides/assets/images/icons/callouts/13.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/14.png b/guides/assets/images/icons/callouts/14.png
deleted file mode 100644
index dbde9ca749..0000000000
--- a/guides/assets/images/icons/callouts/14.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/15.png b/guides/assets/images/icons/callouts/15.png
deleted file mode 100644
index 70e4bba615..0000000000
--- a/guides/assets/images/icons/callouts/15.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/2.png b/guides/assets/images/icons/callouts/2.png
deleted file mode 100644
index 8c57970ba9..0000000000
--- a/guides/assets/images/icons/callouts/2.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/3.png b/guides/assets/images/icons/callouts/3.png
deleted file mode 100644
index 57a33d15b4..0000000000
--- a/guides/assets/images/icons/callouts/3.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/4.png b/guides/assets/images/icons/callouts/4.png
deleted file mode 100644
index f061ab02b8..0000000000
--- a/guides/assets/images/icons/callouts/4.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/5.png b/guides/assets/images/icons/callouts/5.png
deleted file mode 100644
index b4de02da11..0000000000
--- a/guides/assets/images/icons/callouts/5.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/6.png b/guides/assets/images/icons/callouts/6.png
deleted file mode 100644
index 0e055eec1e..0000000000
--- a/guides/assets/images/icons/callouts/6.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/7.png b/guides/assets/images/icons/callouts/7.png
deleted file mode 100644
index 5ead87d040..0000000000
--- a/guides/assets/images/icons/callouts/7.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/8.png b/guides/assets/images/icons/callouts/8.png
deleted file mode 100644
index cb99545eb6..0000000000
--- a/guides/assets/images/icons/callouts/8.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/9.png b/guides/assets/images/icons/callouts/9.png
deleted file mode 100644
index 0ac03602f6..0000000000
--- a/guides/assets/images/icons/callouts/9.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/caution.png b/guides/assets/images/icons/caution.png
deleted file mode 100644
index 7227b54b32..0000000000
--- a/guides/assets/images/icons/caution.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/example.png b/guides/assets/images/icons/example.png
deleted file mode 100644
index a0e855befa..0000000000
--- a/guides/assets/images/icons/example.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/home.png b/guides/assets/images/icons/home.png
deleted file mode 100644
index e70e164522..0000000000
--- a/guides/assets/images/icons/home.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/important.png b/guides/assets/images/icons/important.png
deleted file mode 100644
index bab53bf3aa..0000000000
--- a/guides/assets/images/icons/important.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/next.png b/guides/assets/images/icons/next.png
deleted file mode 100644
index a158832725..0000000000
--- a/guides/assets/images/icons/next.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/note.png b/guides/assets/images/icons/note.png
deleted file mode 100644
index 62eec7845f..0000000000
--- a/guides/assets/images/icons/note.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/prev.png b/guides/assets/images/icons/prev.png
deleted file mode 100644
index 8a96960422..0000000000
--- a/guides/assets/images/icons/prev.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/tip.png b/guides/assets/images/icons/tip.png
deleted file mode 100644
index a5316d318f..0000000000
--- a/guides/assets/images/icons/tip.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/up.png b/guides/assets/images/icons/up.png
deleted file mode 100644
index 6cac818170..0000000000
--- a/guides/assets/images/icons/up.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/warning.png b/guides/assets/images/icons/warning.png
deleted file mode 100644
index 72a8a5d873..0000000000
--- a/guides/assets/images/icons/warning.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/oscardelben.jpg b/guides/assets/images/oscardelben.jpg
deleted file mode 100644
index 9f3f67c2c7..0000000000
--- a/guides/assets/images/oscardelben.jpg
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/polymorphic.png b/guides/assets/images/polymorphic.png
deleted file mode 100644
index e0a7f6d64a..0000000000
--- a/guides/assets/images/polymorphic.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/radar.png b/guides/assets/images/radar.png
deleted file mode 100644
index 421b62b623..0000000000
--- a/guides/assets/images/radar.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/rails_logo_remix.gif b/guides/assets/images/rails_logo_remix.gif
deleted file mode 100644
index 58960ee4f9..0000000000
--- a/guides/assets/images/rails_logo_remix.gif
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/csrf.png b/guides/assets/images/security/csrf.png
index a8123d47c3..a8123d47c3 100644
--- a/guides/assets/images/csrf.png
+++ b/guides/assets/images/security/csrf.png
Binary files differ
diff --git a/guides/assets/images/session_fixation.png b/guides/assets/images/security/session_fixation.png
index e009484f09..e009484f09 100644
--- a/guides/assets/images/session_fixation.png
+++ b/guides/assets/images/security/session_fixation.png
Binary files differ
diff --git a/guides/assets/images/tab_yellow.png b/guides/assets/images/tab_yellow.png
deleted file mode 100644
index 053c807d28..0000000000
--- a/guides/assets/images/tab_yellow.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/vijaydev.jpg b/guides/assets/images/vijaydev.jpg
deleted file mode 100644
index fe5e4f1cb4..0000000000
--- a/guides/assets/images/vijaydev.jpg
+++ /dev/null
Binary files differ
diff --git a/guides/assets/javascripts/guides.js b/guides/assets/javascripts/guides.js
index e4d25dfb21..e39ac239cd 100644
--- a/guides/assets/javascripts/guides.js
+++ b/guides/assets/javascripts/guides.js
@@ -1,53 +1,54 @@
-$.fn.selectGuide = function(guide) {
- $("select", this).val(guide);
-};
-
-var guidesIndex = {
- bind: function() {
- var currentGuidePath = window.location.pathname;
- var currentGuide = currentGuidePath.substring(currentGuidePath.lastIndexOf("/")+1);
- $(".guides-index-small").
- on("change", "select", guidesIndex.navigate).
- selectGuide(currentGuide);
- $(document).on("click", ".more-info-button", function(e){
- e.stopPropagation();
- if ($(".more-info-links").is(":visible")) {
- $(".more-info-links").addClass("s-hidden").unwrap();
- } else {
- $(".more-info-links").wrap("<div class='more-info-container'></div>").removeClass("s-hidden");
- }
- });
- $("#guidesMenu").on("click", function(e) {
- $("#guides").toggle();
- return false;
+(function() {
+ "use strict";
+
+ this.syntaxhighlighterConfig = { autoLinks: false };
+
+ this.wrap = function(elem, wrapper) {
+ elem.parentNode.insertBefore(wrapper, elem);
+ wrapper.appendChild(elem);
+ }
+
+ this.unwrap = function(elem) {
+ var wrapper = elem.parentNode;
+ wrapper.parentNode.replaceChild(elem, wrapper);
+ }
+
+ this.createElement = function(tagName, className) {
+ var elem = document.createElement(tagName);
+ elem.classList.add(className);
+ return elem;
+ }
+
+ document.addEventListener("DOMContentLoaded", function() {
+ var guidesMenu = document.getElementById("guidesMenu");
+ var guides = document.getElementById("guides");
+
+ guidesMenu.addEventListener("click", function(e) {
+ e.preventDefault();
+ guides.classList.toggle("visible");
});
- $(document).on("click", function(e){
- e.stopPropagation();
- var $button = $(".more-info-button");
- var element;
- // Cross browser find the element that had the event
- if (e.target) element = e.target;
- else if (e.srcElement) element = e.srcElement;
+ var guidesIndexItem = document.querySelector("select.guides-index-item");
+ var currentGuidePath = window.location.pathname;
+ guidesIndexItem.value = currentGuidePath.substring(currentGuidePath.lastIndexOf("/") + 1);
- // Defeat the older Safari bug:
- // http://www.quirksmode.org/js/events_properties.html
- if (element.nodeType === 3) element = element.parentNode;
+ guidesIndexItem.addEventListener("change", function(e) {
+ window.location = e.target.value;
+ });
- var $element = $(element);
+ var moreInfoButton = document.querySelector(".more-info-button");
+ var moreInfoLinks = document.querySelector(".more-info-links");
- var $container = $element.parents(".more-info-container");
+ moreInfoButton.addEventListener("click", function(e) {
+ e.preventDefault();
- // We've captured a click outside the popup
- if($container.length === 0){
- $container = $button.next(".more-info-container");
- $container.find(".more-info-links").addClass("s-hidden").unwrap();
+ if (moreInfoLinks.classList.contains("s-hidden")) {
+ wrap(moreInfoLinks, createElement("div", "more-info-container"));
+ moreInfoLinks.classList.remove("s-hidden");
+ } else {
+ moreInfoLinks.classList.add("s-hidden");
+ unwrap(moreInfoLinks);
}
});
- },
- navigate: function(e){
- var $list = $(e.target);
- var url = $list.val();
- window.location = url;
- }
-};
+ });
+}).call(this);
diff --git a/guides/assets/javascripts/jquery.min.js b/guides/assets/javascripts/jquery.min.js
deleted file mode 100644
index 93adea19fd..0000000000
--- a/guides/assets/javascripts/jquery.min.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/*! jQuery v1.7.2 jquery.com | jquery.org/license */
-(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"<!doctype html>":"")+"<html><body>"),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function ca(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function b_(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bD.test(a)?d(a,e):b_(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&f.type(b)==="object")for(var e in b)b_(a+"["+e+"]",b[e],c,d);else d(a,b)}function b$(a,c){var d,e,g=f.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((g[d]?a:e||(e={}))[d]=c[d]);e&&f.extend(!0,a,e)}function bZ(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bS,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bZ(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bZ(a,c,d,e,"*",g));return l}function bY(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bO),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bB(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?1:0,g=4;if(d>0){if(c!=="border")for(;e<g;e+=2)c||(d-=parseFloat(f.css(a,"padding"+bx[e]))||0),c==="margin"?d+=parseFloat(f.css(a,c+bx[e]))||0:d-=parseFloat(f.css(a,"border"+bx[e]+"Width"))||0;return d+"px"}d=by(a,b);if(d<0||d==null)d=a.style[b];if(bt.test(d))return d;d=parseFloat(d)||0;if(c)for(;e<g;e+=2)d+=parseFloat(f.css(a,"padding"+bx[e]))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+bx[e]+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+bx[e]))||0);return d+"px"}function bo(a){var b=c.createElement("div");bh.appendChild(b),b.innerHTML=a.outerHTML;return b.firstChild}function bn(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bm(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bm)}function bm(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bl(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bk(a,b){var c;b.nodeType===1&&(b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase(),c==="object"?b.outerHTML=a.outerHTML:c!=="input"||a.type!=="checkbox"&&a.type!=="radio"?c==="option"?b.selected=a.defaultSelected:c==="input"||c==="textarea"?b.defaultValue=a.defaultValue:c==="script"&&b.text!==a.text&&(b.text=a.text):(a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value)),b.removeAttribute(f.expando),b.removeAttribute("_submit_attached"),b.removeAttribute("_change_attached"))}function bj(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d<e;d++)f.event.add(b,c,i[c][d])}h.data&&(h.data=f.extend({},h.data))}}function bi(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function U(a){var b=V.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function T(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(O.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c<d;c++)b[a[c]]=!0;return b}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b,c){var d;if(b){if(H)return H.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h,i){var j,k=d==null,l=0,m=a.length;if(d&&typeof d=="object"){for(l in d)e.access(a,c,l,d[l],1,h,f);g=1}else if(f!==b){j=i===b&&e.isFunction(f),k&&(j?(j=c,c=function(a,b,c){return j.call(e(a),c)}):(c.call(a,f),c=null));if(c)for(;l<m;l++)c(a[l],d,j?f.call(a[l],l,c(a[l],d)):f,i);g=1}return g?a:k?c.call(a):m?c(a[0],d):h},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g={};f.Callbacks=function(a){a=a?g[a]||h(a):{};var c=[],d=[],e,i,j,k,l,m,n=function(b){var d,e,g,h,i;for(d=0,e=b.length;d<e;d++)g=b[d],h=f.type(g),h==="array"?n(g):h==="function"&&(!a.unique||!p.has(g))&&c.push(g)},o=function(b,f){f=f||[],e=!a.memory||[b,f],i=!0,j=!0,m=k||0,k=0,l=c.length;for(;c&&m<l;m++)if(c[m].apply(b,f)===!1&&a.stopOnFalse){e=!0;break}j=!1,c&&(a.once?e===!0?p.disable():c=[]:d&&d.length&&(e=d.shift(),p.fireWith(e[0],e[1])))},p={add:function(){if(c){var a=c.length;n(arguments),j?l=c.length:e&&e!==!0&&(k=a,o(e[0],e[1]))}return this},remove:function(){if(c){var b=arguments,d=0,e=b.length;for(;d<e;d++)for(var f=0;f<c.length;f++)if(b[d]===c[f]){j&&f<=l&&(l--,f<=m&&m--),c.splice(f--,1);if(a.unique)break}}return this},has:function(a){if(c){var b=0,d=c.length;for(;b<d;b++)if(a===c[b])return!0}return!1},empty:function(){c=[];return this},disable:function(){c=d=e=b;return this},disabled:function(){return!c},lock:function(){d=b,(!e||e===!0)&&p.disable();return this},locked:function(){return!d},fireWith:function(b,c){d&&(j?a.once||d.push([b,c]):(!a.once||!e)&&o(b,c));return this},fire:function(){p.fireWith(this,arguments);return this},fired:function(){return!!i}};return p};var i=[].slice;f.extend({Deferred:function(a){var b=f.Callbacks("once memory"),c=f.Callbacks("once memory"),d=f.Callbacks("memory"),e="pending",g={resolve:b,reject:c,notify:d},h={done:b.add,fail:c.add,progress:d.add,state:function(){return e},isResolved:b.fired,isRejected:c.fired,then:function(a,b,c){i.done(a).fail(b).progress(c);return this},always:function(){i.done.apply(i,arguments).fail.apply(i,arguments);return this},pipe:function(a,b,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[b,"reject"],progress:[c,"notify"]},function(a,b){var c=b[0],e=b[1],g;f.isFunction(c)?i[a](function(){g=c.apply(this,arguments),g&&f.isFunction(g.promise)?g.promise().then(d.resolve,d.reject,d.notify):d[e+"With"](this===i?d:this,[g])}):i[a](d[e])})}).promise()},promise:function(a){if(a==null)a=h;else for(var b in h)a[b]=h[b];return a}},i=h.promise({}),j;for(j in g)i[j]=g[j].fire,i[j+"With"]=g[j].fireWith;i.done(function(){e="resolved"},c.disable,d.lock).fail(function(){e="rejected"},b.disable,d.lock),a&&a.call(i,i);return i},when:function(a){function m(a){return function(b){e[a]=arguments.length>1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c<d;c++)b[c]&&b[c].promise&&f.isFunction(b[c].promise)?b[c].promise().then(l(c),j.reject,m(c)):--g;g||j.resolveWith(j,b)}else j!==a&&j.resolveWith(j,d?[a]:[]);return k}}),f.support=function(){var b,d,e,g,h,i,j,k,l,m,n,o,p=c.createElement("div"),q=c.documentElement;p.setAttribute("className","t"),p.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="<div "+n+"display:block;'><div style='"+t+"0;display:block;overflow:hidden;'></div></div>"+"<table "+n+"' cellpadding='0' cellspacing='0'>"+"<tr><td></td></tr></table>",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="<table><tr><td style='"+t+"0;display:none'></td><td>t</td></tr></table>",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="<div style='width:5px;'></div>",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e<g;e++)delete d[b[e]];if(!(c?m:f.isEmptyObject)(d))return}}if(!c){delete j[k].data;if(!m(j[k]))return}f.support.deleteExpando||!j.setInterval?delete j[k]:j[k]=null,i&&(f.support.deleteExpando?delete a[h]:a.removeAttribute?a.removeAttribute(h):a[h]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d,e,g,h,i,j=this[0],k=0,m=null;if(a===b){if(this.length){m=f.data(j);if(j.nodeType===1&&!f._data(j,"parsedAttrs")){g=j.attributes;for(i=g.length;k<i;k++)h=g[k].name,h.indexOf("data-")===0&&(h=f.camelCase(h.substring(5)),l(j,h,m[h]));f._data(j,"parsedAttrs",!0)}}return m}if(typeof a=="object")return this.each(function(){f.data(this,a)});d=a.split(".",2),d[1]=d[1]?"."+d[1]:"",e=d[1]+"!";return f.access(this,function(c){if(c===b){m=this.triggerHandler("getData"+e,[d[0]]),m===b&&j&&(m=f.data(j,a),m=l(j,a,m));return m===b&&d[1]?this.data(d[0]):m}d[1]=c,this.each(function(){var b=f(this);b.triggerHandler("setData"+e,d),f.data(this,a,c),b.triggerHandler("changeData"+e,d)})},null,c,arguments.length>1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length<d)return f.queue(this[0],a);return c===b?this:this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f.Callbacks("once memory"),!0))h++,l.add(m);m();return d.promise(c)}});var o=/[\n\t\r]/g,p=/\s+/,q=/\r/g,r=/^(?:button|input)$/i,s=/^(?:button|input|object|select|textarea)$/i,t=/^a(?:rea)?$/i,u=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,v=f.support.getSetAttribute,w,x,y;f.fn.extend({attr:function(a,b){return f.access(this,f.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(p);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(o," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(p);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(o," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c<d;c++){e=i[c];if(e.selected&&(f.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!f.nodeName(e.parentNode,"optgroup"))){b=f(e).val();if(j)return b;h.push(b)}}if(j&&!h.length&&i.length)return f(i[g]).val();return h},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i<g;i++)e=d[i],e&&(c=f.propFix[e]||e,h=u.test(e),h||f.attr(a,e,""),a.removeAttribute(v?e:c),h&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(r.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(w&&f.nodeName(a,"button"))return w.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(w&&f.nodeName(a,"button"))return w.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,g,h,i=a.nodeType;if(!!a&&i!==3&&i!==8&&i!==2){h=i!==1||!f.isXMLDoc(a),h&&(c=f.propFix[c]||c,g=f.propHooks[c]);return d!==b?g&&"set"in g&&(e=g.set(a,d,c))!==b?e:a[c]=d:g&&"get"in g&&(e=g.get(a,c))!==null?e:a[c]}},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):s.test(a.nodeName)||t.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabindex=f.propHooks.tabIndex,x={get:function(a,c){var d,e=f.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},v||(y={name:!0,id:!0,coords:!0},w=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&(y[c]?d.nodeValue!=="":d.specified)?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.attrHooks.tabindex.set=w.set,f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})}),f.attrHooks.contenteditable={get:w.get,set:function(a,b,c){b===""&&(b="false"),w.set(a,b,c)}}),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.enctype||(f.propFix.enctype="encoding"),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(
-a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k<c.length;k++){l=A.exec(c[k])||[],m=l[1],n=(l[2]||"").split(".").sort(),s=f.event.special[m]||{},m=(g?s.delegateType:s.bindType)||m,s=f.event.special[m]||{},o=f.extend({type:m,origType:l[1],data:e,handler:d,guid:d.guid,selector:g,quick:g&&G(g),namespace:n.join(".")},p),r=j[m];if(!r){r=j[m]=[],r.delegateCount=0;if(!s.setup||s.setup.call(a,e,n,i)===!1)a.addEventListener?a.addEventListener(m,i,!1):a.attachEvent&&a.attachEvent("on"+m,i)}s.add&&(s.add.call(a,o),o.handler.guid||(o.handler.guid=d.guid)),g?r.splice(r.delegateCount++,0,o):r.push(o),f.event.global[m]=!0}a=null}},global:{},remove:function(a,b,c,d,e){var g=f.hasData(a)&&f._data(a),h,i,j,k,l,m,n,o,p,q,r,s;if(!!g&&!!(o=g.events)){b=f.trim(I(b||"")).split(" ");for(h=0;h<b.length;h++){i=A.exec(b[h])||[],j=k=i[1],l=i[2];if(!j){for(j in o)f.event.remove(a,j+b[h],c,d,!0);continue}p=f.event.special[j]||{},j=(d?p.delegateType:p.bindType)||j,r=o[j]||[],m=r.length,l=l?new RegExp("(^|\\.)"+l.split(".").sort().join("\\.(?:.*\\.)?")+"(\\.|$)"):null;for(n=0;n<r.length;n++)s=r[n],(e||k===s.origType)&&(!c||c.guid===s.guid)&&(!l||l.test(s.namespace))&&(!d||d===s.selector||d==="**"&&s.selector)&&(r.splice(n--,1),s.selector&&r.delegateCount--,p.remove&&p.remove.call(a,s));r.length===0&&m!==r.length&&((!p.teardown||p.teardown.call(a,l)===!1)&&f.removeEvent(a,j,g.handle),delete o[j])}f.isEmptyObject(o)&&(q=g.handle,q&&(q.elem=null),f.removeData(a,["events","handle"],!0))}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){if(!e||e.nodeType!==3&&e.nodeType!==8){var h=c.type||c,i=[],j,k,l,m,n,o,p,q,r,s;if(E.test(h+f.event.triggered))return;h.indexOf("!")>=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l<r.length&&!c.isPropagationStopped();l++)m=r[l][0],c.type=r[l][1],q=(f._data(m,"events")||{})[c.type]&&f._data(m,"handle"),q&&q.apply(m,d),q=o&&m[o],q&&f.acceptData(m)&&q.apply(m,d)===!1&&c.preventDefault();c.type=h,!g&&!c.isDefaultPrevented()&&(!p._default||p._default.apply(e.ownerDocument,d)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)&&o&&e[h]&&(h!=="focus"&&h!=="blur"||c.target.offsetWidth!==0)&&!f.isWindow(e)&&(n=e[o],n&&(e[o]=null),f.event.triggered=h,e[h](),f.event.triggered=b,n&&(e[o]=n));return c.result}},dispatch:function(c){c=f.event.fix(c||a.event);var d=(f._data(this,"events")||{})[c.type]||[],e=d.delegateCount,g=[].slice.call(arguments,0),h=!c.exclusive&&!c.namespace,i=f.event.special[c.type]||{},j=[],k,l,m,n,o,p,q,r,s,t,u;g[0]=c,c.delegateTarget=this;if(!i.preDispatch||i.preDispatch.call(this,c)!==!1){if(e&&(!c.button||c.type!=="click")){n=f(this),n.context=this.ownerDocument||this;for(m=c.target;m!=this;m=m.parentNode||this)if(m.disabled!==!0){p={},r=[],n[0]=m;for(k=0;k<e;k++)s=d[k],t=s.selector,p[t]===b&&(p[t]=s.quick?H(m,s.quick):n.is(t)),p[t]&&r.push(s);r.length&&j.push({elem:m,matches:r})}}d.length>e&&j.push({elem:this,matches:d.slice(e)});for(k=0;k<j.length&&!c.isPropagationStopped();k++){q=j[k],c.currentTarget=q.elem;for(l=0;l<q.matches.length&&!c.isImmediatePropagationStopped();l++){s=q.matches[l];if(h||!c.namespace&&!s.namespace||c.namespace_re&&c.namespace_re.test(s.namespace))c.data=s.data,c.handleObj=s,o=((f.event.special[s.origType]||{}).handle||s.handler).apply(q.elem,g),o!==b&&(c.result=o,o===!1&&(c.preventDefault(),c.stopPropagation()))}}i.postDispatch&&i.postDispatch.call(this,c);return c.result}},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode);return a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,d){var e,f,g,h=d.button,i=d.fromElement;a.pageX==null&&d.clientX!=null&&(e=a.target.ownerDocument||c,f=e.documentElement,g=e.body,a.pageX=d.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=d.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?d.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0);return a}},fix:function(a){if(a[f.expando])return a;var d,e,g=a,h=f.event.fixHooks[a.type]||{},i=h.props?this.props.concat(h.props):this.props;a=f.Event(g);for(d=i.length;d;)e=i[--d],a[e]=g[e];a.target||(a.target=g.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey===b&&(a.metaKey=a.ctrlKey);return h.filter?h.filter(a,g):a},special:{ready:{setup:f.bindReady},load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=f.extend(new f.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?f.event.trigger(e,null,b):f.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},f.event.handle=f.event.dispatch,f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!(this instanceof f.Event))return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?K:J):this.type=a,b&&f.extend(this,b),this.timeStamp=a&&a.timeStamp||f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=K;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=K;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=K,this.stopPropagation()},isDefaultPrevented:J,isPropagationStopped:J,isImmediatePropagationStopped:J},f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c=this,d=a.relatedTarget,e=a.handleObj,g=e.selector,h;if(!d||d!==c&&!f.contains(c,d))a.type=e.origType,h=e.handler.apply(this,arguments),a.type=b;return h}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(){if(f.nodeName(this,"form"))return!1;f.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=f.nodeName(c,"input")||f.nodeName(c,"button")?c.form:b;d&&!d._submit_attached&&(f.event.add(d,"submit._submit",function(a){a._submit_bubble=!0}),d._submit_attached=!0)})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&f.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){if(f.nodeName(this,"form"))return!1;f.event.remove(this,"._submit")}}),f.support.changeBubbles||(f.event.special.change={setup:function(){if(z.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")f.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),f.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1,f.event.simulate("change",this,a,!0))});return!1}f.event.add(this,"beforeactivate._change",function(a){var b=a.target;z.test(b.nodeName)&&!b._change_attached&&(f.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&f.event.simulate("change",this.parentNode,a,!0)}),b._change_attached=!0)})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){f.event.remove(this,"._change");return z.test(this.nodeName)}}),f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){var d=0,e=function(a){f.event.simulate(b,a.target,f.event.fix(a),!0)};f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.fn.extend({on:function(a,c,d,e,g){var h,i;if(typeof a=="object"){typeof c!="string"&&(d=d||c,c=b);for(i in a)this.on(i,c,d,a[i],g);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=J;else if(!e)return this;g===1&&(h=e,e=function(a){f().off(a);return h.apply(this,arguments)},e.guid=h.guid||(h.guid=f.guid++));return this.each(function(){f.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,c,d){if(a&&a.preventDefault&&a.handleObj){var e=a.handleObj;f(a.delegateTarget).off(e.namespace?e.origType+"."+e.namespace:e.origType,e.selector,e.handler);return this}if(typeof a=="object"){for(var g in a)this.off(g,c,a[g]);return this}if(c===!1||typeof c=="function")d=c,c=b;d===!1&&(d=J);return this.each(function(){f.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){f(this.context).on(a,this.selector,b,c);return this},die:function(a,b){f(this.context).off(a,this.selector||"**",b);return this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length==1?this.off(a,"**"):this.off(b,a,c)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f._data(this,"lastToggle"+a.guid)||0)%d;f._data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}if(j.nodeType===1){g||(j[d]=c,j.sizset=h);if(typeof b!="string"){if(j===b){k=!0;break}}else if(m.filter(b,[j]).length>0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}j.nodeType===1&&!g&&(j[d]=c,j.sizset=h);if(j.nodeName.toLowerCase()===b){k=j;break}j=j[a]}e[h]=k}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},m.matches=function(a,b){return m(a,null,null,b)},m.matchesSelector=function(a,b){return m(b,null,null,[a]).length>0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e<f;e++){h=o.order[e];if(g=o.leftMatch[h].exec(a)){i=g[1],g.splice(1,1);if(i.substr(i.length-1)!=="\\"){g[1]=(g[1]||"").replace(j,""),d=o.find[h](g,b,c);if(d!=null){a=a.replace(o.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},m.filter=function(a,c,d,e){var f,g,h,i,j,k,l,n,p,q=a,r=[],s=c,t=c&&c[0]&&m.isXML(c[0]);while(a&&c.length){for(h in o.filter)if((f=o.leftMatch[h].exec(a))!=null&&f[2]){k=o.filter[h],l=f[1],g=!1,f.splice(1,1);if(l.substr(l.length-1)==="\\")continue;s===r&&(r=[]);if(o.preFilter[h]){f=o.preFilter[h](f,s,d,r,e,t);if(!f)g=i=!0;else if(f===!0)continue}if(f)for(n=0;(j=s[n])!=null;n++)j&&(i=k(j,f,n,s),p=e^i,d&&i!=null?p?g=!0:s[n]=!1:p&&(r.push(j),g=!0));if(i!==b){d||(s=r),a=a.replace(o.match[h],"");if(!g)return[];break}}if(a===q)if(g==null)m.error(a);else break;q=a}return s},m.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)};var n=m.getText=function(a){var b,c,d=a.nodeType,e="";if(d){if(d===1||d===9||d===11){if(typeof a.textContent=="string")return a.textContent;if(typeof a.innerText=="string")return a.innerText.replace(k,"");for(a=a.firstChild;a;a=a.nextSibling)e+=n(a)}else if(d===3||d===4)return a.nodeValue}else for(b=0;c=a[b];b++)c.nodeType!==8&&(e+=n(c));return e},o=m.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!l.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&m.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&m.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(j,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}m.error(e)},CHILD:function(a,b){var c,e,f,g,h,i,j,k=b[1],l=a;switch(k){case"only":case"first":while(l=l.previousSibling)if(l.nodeType===1)return!1;if(k==="first")return!0;l=a;case"last":while(l=l.nextSibling)if(l.nodeType===1)return!1;return!0;case"nth":c=b[2],e=b[3];if(c===1&&e===0)return!0;f=b[0],g=a.parentNode;if(g&&(g[d]!==f||!a.nodeIndex)){i=0;for(l=g.firstChild;l;l=l.nextSibling)l.nodeType===1&&(l.nodeIndex=++i);g[d]=f}j=a.nodeIndex-e;return c===0?j===0:j%c===0&&j/c>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var u,v;c.documentElement.compareDocumentPosition?u=function(a,b){if(a===b){h=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(u=function(a,b){if(a===b){h=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,i=b.parentNode,j=g;if(g===i)return v(a,b);if(!g)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return v(e[k],f[k]);return k===c?v(a,f[k],-1):v(e[k],b,1)},v=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h<i;h++)m(a,g[h],e,c);return m.filter(f,e)};m.attr=f.attr,m.selectors.attrMap={},f.find=m,f.expr=m.selectors,f.expr[":"]=f.expr.filters,f.unique=m.uniqueSort,f.text=m.getText,f.isXMLDoc=m.isXML,f.contains=m.contains}();var L=/Until$/,M=/^(?:parents|prevUntil|prevAll)/,N=/,/,O=/^.[^:#\[\.,]*$/,P=Array.prototype.slice,Q=f.expr.match.globalPOS,R={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(T(this,a,!1),"not",a)},filter:function(a){return this.pushStack(T(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?Q.test(a)?f(a,this.context).index(this[0])>=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d<a.length;d++)f(g).is(a[d])&&c.push({selector:a[d],elem:g,level:h});g=g.parentNode,h++}return c}var i=Q.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(i?i.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/<tbody/i,_=/<|&#?\w+;/,ba=/<(?:script|style)/i,bb=/<(?:script|object|embed|option|style)/i,bc=new RegExp("<(?:"+V+")[\\s/>]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*<!(?:\[CDATA\[|\-\-)/,bg={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f
-.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1></$2>");try{for(;d<e;d++)c=this[d]||{},c.nodeType===1&&(f.cleanData(c.getElementsByTagName("*")),c.innerHTML=a);c=0}catch(g){}}c&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bd.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bi(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,function(a,b){b.src?f.ajax({type:"GET",global:!1,url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bf,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)})}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i,j=a[0];b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof j=="string"&&j.length<512&&i===c&&j.charAt(0)==="<"&&!bb.test(j)&&(f.support.checkClone||!bd.test(j))&&(f.support.html5Clone||!bc.test(j))&&(g=!0,h=f.fragments[j],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[j]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1></$2>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]==="<table>"&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i<u;i++)bn(l[i]);else bn(l);l.nodeType?j.push(l):j=f.merge(j,l)}if(d){g=function(a){return!a.type||be.test(a.type)};for(k=0;j[k];k++){h=j[k];if(e&&f.nodeName(h,"script")&&(!h.type||be.test(h.type)))e.push(h.parentNode?h.parentNode.removeChild(h):h);else{if(h.nodeType===1){var v=f.grep(h.getElementsByTagName("script"),g);j.splice.apply(j,[k+1,0].concat(v))}d.appendChild(h)}}}return j},cleanData:function(a){var b,c,d=f.cache,e=f.event.special,g=f.support.deleteExpando;for(var h=0,i;(i=a[h])!=null;h++){if(i.nodeName&&f.noData[i.nodeName.toLowerCase()])continue;c=i[f.expando];if(c){b=d[c];if(b&&b.events){for(var j in b.events)e[j]?f.event.remove(i,j):f.removeEvent(i,j,b.handle);b.handle&&(b.handle.elem=null)}g?delete i[f.expando]:i.removeAttribute&&i.removeAttribute(f.expando),delete d[c]}}}});var bp=/alpha\([^)]*\)/i,bq=/opacity=([^)]*)/,br=/([A-Z]|^ms)/g,bs=/^[\-+]?(?:\d*\.)?\d+$/i,bt=/^-?(?:\d*\.)?\d+(?!px)[^\d\s]+$/i,bu=/^([\-+])=([\-+.\de]+)/,bv=/^margin/,bw={position:"absolute",visibility:"hidden",display:"block"},bx=["Top","Right","Bottom","Left"],by,bz,bA;f.fn.css=function(a,c){return f.access(this,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)},a,c,arguments.length>1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),(e===""&&f.css(d,"display")==="none"||!f.contains(d.ownerDocument.documentElement,d))&&f._data(d,"olddisplay",cu(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(ct("hide",3),a,b,c);var d,e,g=0,h=this.length;for(;g<h;g++)d=this[g],d.style&&(e=f.css(d,"display"),e!=="none"&&!f._data(d,"olddisplay")&&f._data(d,"olddisplay",e));for(g=0;g<h;g++)this[g].style&&(this[g].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(ct("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){function g(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o,p,q;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]);if((k=f.cssHooks[g])&&"expand"in k){l=k.expand(a[g]),delete a[g];for(i in l)i in a||(a[i]=l[i])}}for(g in a){h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(!f.support.inlineBlockNeedsLayout||cu(this.nodeName)==="inline"?this.style.display="inline-block":this.style.zoom=1))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)j=new f.fx(this,b,i),h=a[i],cm.test(h)?(q=f._data(this,"toggle"+i)||(h==="toggle"?d?"show":"hide":0),q?(f._data(this,"toggle"+i,q==="show"?"hide":"show"),j[q]()):j[h]()):(m=cn.exec(h),n=j.cur(),m?(o=parseFloat(m[2]),p=m[3]||(f.cssNumber[i]?"":"px"),p!=="px"&&(f.style(this,i,(o||1)+p),n=(o||1)/j.cur()*n,f.style(this,i,n+p)),m[1]&&(o=(m[1]==="-="?-1:1)*o+n),j.custom(n,o,p)):j.custom(n,h,""));return!0}var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return e.queue===!1?this.each(g):this.queue(e.queue,g)},stop:function(a,c,d){typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]);return this.each(function(){function h(a,b,c){var e=b[c];f.removeData(a,c,!0),e.stop(d)}var b,c=!1,e=f.timers,g=f._data(this);d||f._unmark(!0,this);if(a==null)for(b in g)g[b]&&g[b].stop&&b.indexOf(".run")===b.length-4&&h(this,g,b);else g[b=a+".run"]&&g[b].stop&&h(this,g,b);for(b=e.length;b--;)e[b].elem===this&&(a==null||e[b].queue===a)&&(d?e[b](!0):e[b].saveState(),c=!0,e.splice(b,1));(!d||!c)&&f.dequeue(this,a)})}}),f.each({slideDown:ct("show",1),slideUp:ct("hide",1),slideToggle:ct("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue?f.dequeue(this,d.queue):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a){return a},swing:function(a){return-Math.cos(a*Math.PI)/2+.5}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,c,d){function h(a){return e.step(a)}var e=this,g=f.fx;this.startTime=cq||cr(),this.end=c,this.now=this.start=a,this.pos=this.state=0,this.unit=d||this.unit||(f.cssNumber[this.prop]?"":"px"),h.queue=this.options.queue,h.elem=this.elem,h.saveState=function(){f._data(e.elem,"fxshow"+e.prop)===b&&(e.options.hide?f._data(e.elem,"fxshow"+e.prop,e.start):e.options.show&&f._data(e.elem,"fxshow"+e.prop,e.end))},h()&&f.timers.push(h)&&!co&&(co=setInterval(g.tick,g.interval))},show:function(){var a=f._data(this.elem,"fxshow"+this.prop);this.options.orig[this.prop]=a||f.style(this.elem,this.prop),this.options.show=!0,a!==b?this.custom(this.cur(),a):this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f._data(this.elem,"fxshow"+this.prop)||f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b,c,d,e=cq||cr(),g=!0,h=this.elem,i=this.options;if(a||e>=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||f.fx.stop()},interval:13,stop:function(){clearInterval(co),co=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=a.now+a.unit:a.elem[a.prop]=a.now}}}),f.each(cp.concat.apply([],cp),function(a,b){b.indexOf("margin")&&(f.fx.step[b]=function(a){f.style(a.elem,b,Math.max(0,a.now)+a.unit)})}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cv,cw=/^t(?:able|d|h)$/i,cx=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?cv=function(a,b,c,d){try{d=a.getBoundingClientRect()}catch(e){}if(!d||!f.contains(c,a))return d?{top:d.top,left:d.left}:{top:0,left:0};var g=b.body,h=cy(b),i=c.clientTop||g.clientTop||0,j=c.clientLeft||g.clientLeft||0,k=h.pageYOffset||f.support.boxModel&&c.scrollTop||g.scrollTop,l=h.pageXOffset||f.support.boxModel&&c.scrollLeft||g.scrollLeft,m=d.top+k-i,n=d.left+l-j;return{top:m,left:n}}:cv=function(a,b,c){var d,e=a.offsetParent,g=a,h=b.body,i=b.defaultView,j=i?i.getComputedStyle(a,null):a.currentStyle,k=a.offsetTop,l=a.offsetLeft;while((a=a.parentNode)&&a!==h&&a!==c){if(f.support.fixedPosition&&j.position==="fixed")break;d=i?i.getComputedStyle(a,null):a.currentStyle,k-=a.scrollTop,l-=a.scrollLeft,a===e&&(k+=a.offsetTop,l+=a.offsetLeft,f.support.doesNotAddBorder&&(!f.support.doesAddBorderForTableAndCells||!cw.test(a.nodeName))&&(k+=parseFloat(d.borderTopWidth)||0,l+=parseFloat(d.borderLeftWidth)||0),g=e,e=a.offsetParent),f.support.subtractsBorderForOverflowNotVisible&&d.overflow!=="visible"&&(k+=parseFloat(d.borderTopWidth)||0,l+=parseFloat(d.borderLeftWidth)||0),j=d}if(j.position==="relative"||j.position==="static")k+=h.offsetTop,l+=h.offsetLeft;f.support.fixedPosition&&j.position==="fixed"&&(k+=Math.max(c.scrollTop,h.scrollTop),l+=Math.max(c.scrollLeft,h.scrollLeft));return{top:k,left:l}},f.fn.offset=function(a){if(arguments.length)return a===b?this:this.each(function(b){f.offset.setOffset(this,a,b)});var c=this[0],d=c&&c.ownerDocument;if(!d)return null;if(c===d.body)return f.offset.bodyOffset(c);return cv(c,d,d.documentElement)},f.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file
diff --git a/guides/assets/javascripts/responsive-tables.js b/guides/assets/javascripts/responsive-tables.js
index 8554a1343b..24906dddeb 100644
--- a/guides/assets/javascripts/responsive-tables.js
+++ b/guides/assets/javascripts/responsive-tables.js
@@ -1,43 +1,50 @@
-$(document).ready(function() {
+(function() {
+ "use strict";
+
var switched = false;
- $("table").not(".syntaxhighlighter").addClass("responsive");
+
+ // For old browsers
+ var each = function(node, callback) {
+ var array = Array.prototype.slice.call(node);
+ for(var i = 0; i < array.length; i++) callback(array[i]);
+ }
+
+ each(document.querySelectorAll(":not(.syntaxhighlighter)>table"), function(element) {
+ element.classList.add("responsive");
+ });
+
var updateTables = function() {
- if (($(window).width() < 767) && !switched ){
+ if (document.documentElement.clientWidth < 767 && !switched) {
switched = true;
- $("table.responsive").each(function(i, element) {
- splitTable($(element));
- });
- return true;
- }
- else if (switched && ($(window).width() > 767)) {
+ each(document.querySelectorAll("table.responsive"), splitTable);
+ } else {
switched = false;
- $("table.responsive").each(function(i, element) {
- unsplitTable($(element));
- });
+ each(document.querySelectorAll(".table-wrapper table.responsive"), unsplitTable);
}
- };
-
- $(window).load(updateTables);
- $(window).bind("resize", updateTables);
-
-
- function splitTable(original)
- {
- original.wrap("<div class='table-wrapper' />");
-
- var copy = original.clone();
- copy.find("td:not(:first-child), th:not(:first-child)").css("display", "none");
- copy.removeClass("responsive");
-
- original.closest(".table-wrapper").append(copy);
- copy.wrap("<div class='pinned' />");
- original.wrap("<div class='scrollable' />");
- }
-
- function unsplitTable(original) {
- original.closest(".table-wrapper").find(".pinned").remove();
- original.unwrap();
- original.unwrap();
- }
-
-});
+ }
+
+ document.addEventListener("DOMContentLoaded", updateTables);
+ window.addEventListener("resize", updateTables);
+
+ var splitTable = function(original) {
+ wrap(original, createElement("div", "table-wrapper"));
+
+ var copy = original.cloneNode(true);
+ each(copy.querySelectorAll("td:not(:first-child), th:not(:first-child)"), function(element) {
+ element.style.display = "none";
+ });
+ copy.classList.remove("responsive");
+
+ original.parentNode.append(copy);
+ wrap(copy, createElement("div", "pinned"))
+ wrap(original, createElement("div", "scrollable"));
+ }
+
+ var unsplitTable = function(original) {
+ each(document.querySelectorAll(".table-wrapper .pinned"), function(element) {
+ element.parentNode.removeChild(element);
+ });
+ unwrap(original.parentNode);
+ unwrap(original);
+ }
+}).call(this);
diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css
index b27776745a..00d4bcb21e 100644
--- a/guides/assets/stylesheets/main.css
+++ b/guides/assets/stylesheets/main.css
@@ -70,7 +70,7 @@ table {
}
table th, table td {
- padding: 0.25em 1em;
+ padding: 9px 10px;
border: 1px solid #CCC;
border-collapse: collapse;
}
@@ -79,7 +79,6 @@ table th {
border-bottom: 2px solid #CCC;
background: #EEE;
font-weight: bold;
- padding: 0.5em 1em;
}
img {
@@ -265,8 +264,6 @@ body {
}
}
-#extraCol {display: none;}
-
#footer {
padding: 2em 0;
background: #222 url(../images/footer_tile.gif) repeat-x;
@@ -410,6 +407,10 @@ a, a:link, a:visited {
padding-top: 2em;
}
+#guides.visible {
+ display: block !important;
+}
+
#guides dt, #guides dd {
font-weight: normal;
font-size: 0.722em;
@@ -555,8 +556,6 @@ h6 {
font-size: 1.2857em;
padding: 0.125em 0 0.25em 0;
margin-bottom: 0;
- /*background: url(../images/book_icon.gif) no-repeat left top;
- padding: 0.125em 0 0.25em 28px;*/
}
@media screen and (max-width: 480px) {
@@ -665,10 +664,8 @@ div.code_container {
visibility: hidden;
}
-.clearfix {display: inline-block;}
* html .clearfix {height: 1%;}
.clearfix {display: block;}
-.clear { clear:both; }
/* Same bottom margin for special boxes than for regular paragraphs, this way
intermediate whitespace looks uniform. */
@@ -696,9 +693,6 @@ div.important p, div.caution p, div.warning p, div.note p, div.info p {
/* Foundation v2.1.4 http://foundation.zurb.com */
/* Artfully masterminded by ZURB */
-table th { font-weight: bold; }
-table td, table th { padding: 9px 10px; text-align: left; }
-
/* Mobile */
@media only screen and (max-width: 767px) {
table.responsive { margin-bottom: 0; }
diff --git a/guides/assets/stylesheets/print.css b/guides/assets/stylesheets/print.css
index bdc8ec948d..6280422469 100644
--- a/guides/assets/stylesheets/print.css
+++ b/guides/assets/stylesheets/print.css
@@ -4,7 +4,7 @@
/* Modified January 31, 2009
--------------------------------------- */
-body, .wrapper, .note, .info, code, #topNav, .L, .R, #frame, #container, #header, #navigation, #footer, #feature, #mainCol, #subCol, #extraCol, .content {position: static; text-align: left; text-indent: 0; background: White; color: Black; border-color: Black; width: auto; height: auto; display: block; float: none; min-height: 0; margin: 0; padding: 0;}
+body, .wrapper, .note, .info, code, #topNav, .L, .R, #frame, #container, #header, #navigation, #footer, #feature, #mainCol, #subCol, .content {position: static; text-align: left; text-indent: 0; background: White; color: Black; border-color: Black; width: auto; height: auto; display: block; float: none; min-height: 0; margin: 0; padding: 0;}
body {
background: #FFF;
diff --git a/guides/assets/stylesheets/responsive-tables.css b/guides/assets/stylesheets/responsive-tables.css
deleted file mode 100644
index f5fbcbf948..0000000000
--- a/guides/assets/stylesheets/responsive-tables.css
+++ /dev/null
@@ -1,50 +0,0 @@
-/* Foundation v2.1.4 http://foundation.zurb.com */
-/* Artfully masterminded by ZURB */
-
-/* --------------------------------------------------
- Table of Contents
------------------------------------------------------
-:: Shared Styles
-:: Page Name 1
-:: Page Name 2
-*/
-
-
-/* -----------------------------------------
- Shared Styles
------------------------------------------ */
-
-table th { font-weight: bold; }
-table td, table th { padding: 9px 10px; text-align: left; }
-
-/* Mobile */
-@media only screen and (max-width: 767px) {
-
- table { margin-bottom: 0; }
-
- .pinned { position: absolute; left: 0; top: 0; background: #fff; width: 35%; overflow: hidden; overflow-x: scroll; border-right: 1px solid #ccc; border-left: 1px solid #ccc; }
- .pinned table { border-right: none; border-left: none; width: 100%; }
- .pinned table th, .pinned table td { white-space: nowrap; }
- .pinned td:last-child { border-bottom: 0; }
-
- div.table-wrapper { position: relative; margin-bottom: 20px; overflow: hidden; border-right: 1px solid #ccc; }
- div.table-wrapper div.scrollable table { margin-left: 35%; }
- div.table-wrapper div.scrollable { overflow: scroll; overflow-y: hidden; }
-
- table td, table th { position: relative; white-space: nowrap; overflow: hidden; }
- table th:first-child, table td:first-child, table td:first-child, table.pinned td { display: none; }
-
-}
-
-/* -----------------------------------------
- Page Name 1
------------------------------------------ */
-
-
-
-
-/* -----------------------------------------
- Page Name 2
------------------------------------------ */
-
-
diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb
index 4d8d8db3e5..e8b6ad19dd 100644
--- a/guides/bug_report_templates/action_controller_gem.rb
+++ b/guides/bug_report_templates/action_controller_gem.rb
@@ -9,8 +9,11 @@ end
gemfile(true) do
source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
# Activate the gem you are reporting the issue against.
- gem "rails", "5.1.0"
+ gem "rails", "5.2.0"
end
require "rack/test"
@@ -19,7 +22,6 @@ require "action_controller/railtie"
class TestApp < Rails::Application
config.root = __dir__
config.session_store :cookie_store, key: "cookie_store_key"
- secrets.secret_token = "secret_token"
secrets.secret_key_base = "secret_key_base"
config.logger = Logger.new($stdout)
diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb
index 1f862e07da..ffd81c0079 100644
--- a/guides/bug_report_templates/action_controller_master.rb
+++ b/guides/bug_report_templates/action_controller_master.rb
@@ -9,15 +9,16 @@ end
gemfile(true) do
source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
gem "rails", github: "rails/rails"
- gem "arel", github: "rails/arel"
end
require "action_controller/railtie"
class TestApp < Rails::Application
config.root = __dir__
- secrets.secret_token = "secret_token"
secrets.secret_key_base = "secret_key_base"
config.logger = Logger.new($stdout)
diff --git a/guides/bug_report_templates/active_job_gem.rb b/guides/bug_report_templates/active_job_gem.rb
index af777a86ef..720b7e9c51 100644
--- a/guides/bug_report_templates/active_job_gem.rb
+++ b/guides/bug_report_templates/active_job_gem.rb
@@ -9,8 +9,11 @@ end
gemfile(true) do
source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
# Activate the gem you are reporting the issue against.
- gem "activejob", "5.1.0"
+ gem "activejob", "5.2.0"
end
require "minitest/autorun"
diff --git a/guides/bug_report_templates/active_job_master.rb b/guides/bug_report_templates/active_job_master.rb
index 39fb3f60a6..4bcee07607 100644
--- a/guides/bug_report_templates/active_job_master.rb
+++ b/guides/bug_report_templates/active_job_master.rb
@@ -9,8 +9,10 @@ end
gemfile(true) do
source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
gem "rails", github: "rails/rails"
- gem "arel", github: "rails/arel"
end
require "active_job"
diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb
index 168e2dcc66..c0d705239b 100644
--- a/guides/bug_report_templates/active_record_gem.rb
+++ b/guides/bug_report_templates/active_record_gem.rb
@@ -9,8 +9,11 @@ end
gemfile(true) do
source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
# Activate the gem you are reporting the issue against.
- gem "activerecord", "5.1.0"
+ gem "activerecord", "5.2.0"
gem "sqlite3"
end
diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb
index cbd2cff2b8..914f04f51a 100644
--- a/guides/bug_report_templates/active_record_master.rb
+++ b/guides/bug_report_templates/active_record_master.rb
@@ -9,8 +9,10 @@ end
gemfile(true) do
source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
gem "rails", github: "rails/rails"
- gem "arel", github: "rails/arel"
gem "sqlite3"
end
diff --git a/guides/bug_report_templates/active_record_migrations_gem.rb b/guides/bug_report_templates/active_record_migrations_gem.rb
index b931ed0beb..f47cf08766 100644
--- a/guides/bug_report_templates/active_record_migrations_gem.rb
+++ b/guides/bug_report_templates/active_record_migrations_gem.rb
@@ -9,8 +9,11 @@ end
gemfile(true) do
source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
# Activate the gem you are reporting the issue against.
- gem "activerecord", "5.1.0"
+ gem "activerecord", "5.2.0"
gem "sqlite3"
end
@@ -34,7 +37,7 @@ end
class Payment < ActiveRecord::Base
end
-class ChangeAmountToAddScale < ActiveRecord::Migration[5.0]
+class ChangeAmountToAddScale < ActiveRecord::Migration[5.2]
def change
reversible do |dir|
dir.up do
diff --git a/guides/bug_report_templates/active_record_migrations_master.rb b/guides/bug_report_templates/active_record_migrations_master.rb
index 2c009c0563..715dca98ba 100644
--- a/guides/bug_report_templates/active_record_migrations_master.rb
+++ b/guides/bug_report_templates/active_record_migrations_master.rb
@@ -9,8 +9,10 @@ end
gemfile(true) do
source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
gem "rails", github: "rails/rails"
- gem "arel", github: "rails/arel"
gem "sqlite3"
end
@@ -34,7 +36,7 @@ end
class Payment < ActiveRecord::Base
end
-class ChangeAmountToAddScale < ActiveRecord::Migration[5.0]
+class ChangeAmountToAddScale < ActiveRecord::Migration[6.0]
def change
reversible do |dir|
dir.up do
diff --git a/guides/bug_report_templates/benchmark.rb b/guides/bug_report_templates/benchmark.rb
index d0f5a634bc..046572148b 100644
--- a/guides/bug_report_templates/benchmark.rb
+++ b/guides/bug_report_templates/benchmark.rb
@@ -9,8 +9,10 @@ end
gemfile(true) do
source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
gem "rails", github: "rails/rails"
- gem "arel", github: "rails/arel"
gem "benchmark-ips"
end
diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb
index c990bda005..0935354bf4 100644
--- a/guides/bug_report_templates/generic_gem.rb
+++ b/guides/bug_report_templates/generic_gem.rb
@@ -9,10 +9,14 @@ end
gemfile(true) do
source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
# Activate the gem you are reporting the issue against.
- gem "activesupport", "5.1.0"
+ gem "activesupport", "5.2.0"
end
+require "active_support"
require "active_support/core_ext/object/blank"
require "minitest/autorun"
diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb
index 1a9b99b624..727f428960 100644
--- a/guides/bug_report_templates/generic_master.rb
+++ b/guides/bug_report_templates/generic_master.rb
@@ -9,8 +9,10 @@ end
gemfile(true) do
source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
gem "rails", github: "rails/rails"
- gem "arel", github: "rails/arel"
end
require "active_support"
diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb
index 7205f37be7..c83538ad48 100644
--- a/guides/rails_guides/generator.rb
+++ b/guides/rails_guides/generator.rb
@@ -141,32 +141,34 @@ module RailsGuides
puts "Generating #{guide} as #{output_file}"
layout = @kindle ? "kindle/layout" : "layout"
- File.open(output_path, "w") do |f|
- view = ActionView::Base.new(
- @source_dir,
- edge: @edge,
- version: @version,
- mobi: "kindle/#{mobi}",
- language: @language
- )
- view.extend(Helpers)
-
- if guide =~ /\.(\w+)\.erb$/
- # Generate the special pages like the home.
- # Passing a template handler in the template name is deprecated. So pass the file name without the extension.
- result = view.render(layout: layout, formats: [$1], file: $`)
- else
- body = File.read("#{@source_dir}/#{guide}")
- result = RailsGuides::Markdown.new(
- view: view,
- layout: layout,
- edge: @edge,
- version: @version
- ).render(body)
-
- warn_about_broken_links(result)
- end
+ view = ActionView::Base.new(
+ @source_dir,
+ edge: @edge,
+ version: @version,
+ mobi: "kindle/#{mobi}",
+ language: @language
+ )
+ view.extend(Helpers)
+
+ if guide =~ /\.(\w+)\.erb$/
+ return if %w[_license _welcome layout].include?($`)
+
+ # Generate the special pages like the home.
+ # Passing a template handler in the template name is deprecated. So pass the file name without the extension.
+ result = view.render(layout: layout, formats: [$1], file: $`)
+ else
+ body = File.read("#{@source_dir}/#{guide}")
+ result = RailsGuides::Markdown.new(
+ view: view,
+ layout: layout,
+ edge: @edge,
+ version: @version
+ ).render(body)
+
+ warn_about_broken_links(result)
+ end
+ File.open(output_path, "w") do |f|
f.write(result)
end
end
diff --git a/guides/rails_guides/helpers.rb b/guides/rails_guides/helpers.rb
index a6970fb90c..5ab1388c29 100644
--- a/guides/rails_guides/helpers.rb
+++ b/guides/rails_guides/helpers.rb
@@ -38,15 +38,6 @@ module RailsGuides
end
end
- def author(name, nick, image = "credits_pic_blank.gif", &block)
- image = "images/#{image}"
-
- result = tag(:img, src: image, class: "left pic", alt: name, width: 91, height: 91)
- result << content_tag(:h3, name)
- result << content_tag(:p, capture(&block))
- content_tag(:div, result, class: "clearfix", id: nick)
- end
-
def code(&block)
c = capture(&block)
content_tag(:code, c)
diff --git a/guides/rails_guides/kindle.rb b/guides/rails_guides/kindle.rb
index 87a369a15a..d370541d2e 100644
--- a/guides/rails_guides/kindle.rb
+++ b/guides/rails_guides/kindle.rb
@@ -35,7 +35,7 @@ module Kindle
def generate_front_matter(html_pages)
frontmatter = []
html_pages.delete_if { |x|
- if x =~ /(toc|welcome|credits|copyright).html/
+ if x =~ /(toc|welcome|copyright).html/
frontmatter << x unless x =~ /toc/
true
end
@@ -58,9 +58,9 @@ module Kindle
end
def generate_sections(html_pages)
- FileUtils::rm_rf("sections/")
+ FileUtils.rm_rf("sections/")
html_pages.each_with_index do |page, section_idx|
- FileUtils::mkdir_p("sections/%03d" % section_idx)
+ FileUtils.mkdir_p("sections/%03d" % section_idx)
doc = Nokogiri::HTML(File.open(page))
title = doc.at("title").inner_text.gsub("Ruby on Rails Guides: ", "")
title = page.capitalize.gsub(".html", "") if title.strip == ""
diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb
index bafa6bfe9d..c48af797fa 100644
--- a/guides/rails_guides/levenshtein.rb
+++ b/guides/rails_guides/levenshtein.rb
@@ -38,7 +38,7 @@ module RailsGuides
d[m] = x
end
- return x
+ x
end
end
end
diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb
index 1f2fe91ea1..78820a7856 100644
--- a/guides/rails_guides/markdown/renderer.rb
+++ b/guides/rails_guides/markdown/renderer.rb
@@ -75,7 +75,7 @@ HTML
#
# It is important that we do not eat more than one newline
# because formatting may be wrong otherwise. For example,
- # if a bulleted list follows the first item is not rendered
+ # if a bulleted list follows, the first item is not rendered
# as a list item, but as a paragraph starting with a plain
# asterisk.
body.gsub(/^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:](.*?)(\n(?=\n)|\Z)/m) do
diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md
index ac5833e069..8b91b4853f 100644
--- a/guides/source/2_2_release_notes.md
+++ b/guides/source/2_2_release_notes.md
@@ -57,7 +57,6 @@ rake doc:guides
This will put the guides inside `Rails.root/doc/guides` and you may start surfing straight away by opening `Rails.root/doc/guides/index.html` in your favourite browser.
-* Lead Contributors: [Rails Documentation Team](credits.html)
* Major contributions from [Xavier Noria](http://advogato.org/person/fxn/diary.html) and [Hongli Lai](http://izumi.plan99.net/blog/).
* More information:
* [Rails Guides hackfest](http://hackfest.rubyonrails.org/guide)
@@ -125,7 +124,7 @@ There are two big additions to talk about here: transactional migrations and poo
Historically, multiple-step Rails migrations have been a source of trouble. If something went wrong during a migration, everything before the error changed the database and everything after the error wasn't applied. Also, the migration version was stored as having been executed, which means that it couldn't be simply rerun by `rake db:migrate:redo` after you fix the problem. Transactional migrations change this by wrapping migration steps in a DDL transaction, so that if any of them fail, the entire migration is undone. In Rails 2.2, transactional migrations are supported on PostgreSQL out of the box. The code is extensible to other database types in the future - and IBM has already extended it to support the DB2 adapter.
-* Lead Contributor: [Adam Wiggins](http://adam.heroku.com/)
+* Lead Contributor: [Adam Wiggins](http://about.adamwiggins.com/)
* More information:
* [DDL Transactions](http://adam.heroku.com/past/2008/9/3/ddl_transactions/)
* [A major milestone for DB2 on Rails](http://db2onrails.com/2008/11/08/a-major-milestone-for-db2-on-rails/)
@@ -391,7 +390,7 @@ You can unpack or install a single gem by specifying `GEM=_gem_name_` on the com
* Lead Contributor: [Matt Jones](https://github.com/al2o3cr)
* More information:
* [What's New in Edge Rails: Gem Dependencies](http://archives.ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-gem-dependencies)
- * [Rails 2.1.2 and 2.2RC1: Update Your RubyGems](http://afreshcup.com/2008/10/25/rails-212-and-22rc1-update-your-rubygems/)
+ * [Rails 2.1.2 and 2.2RC1: Update Your RubyGems](https://afreshcup.com/home/2008/10/25/rails-212-and-22rc1-update-your-rubygems)
* [Detailed discussion on Lighthouse](http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/1128)
### Other Railties Changes
diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md
index 3f5a3c7ade..634569fa2d 100644
--- a/guides/source/2_3_release_notes.md
+++ b/guides/source/2_3_release_notes.md
@@ -231,10 +231,10 @@ Rails chooses between file, template, and action depending on whether there is a
### Application Controller Renamed
-If you're one of the people who has always been bothered by the special-case naming of `application.rb`, rejoice! It's been reworked to be application_controller.rb in Rails 2.3. In addition, there's a new rake task, `rake rails:update:application_controller` to do this automatically for you - and it will be run as part of the normal `rake rails:update` process.
+If you're one of the people who has always been bothered by the special-case naming of `application.rb`, rejoice! It's been reworked to be `application_controller.rb` in Rails 2.3. In addition, there's a new rake task, `rake rails:update:application_controller` to do this automatically for you - and it will be run as part of the normal `rake rails:update` process.
* More Information:
- * [The Death of Application.rb](http://afreshcup.com/2008/11/17/rails-2x-the-death-of-applicationrb/)
+ * [The Death of Application.rb](https://afreshcup.com/home/2008/11/17/rails-2x-the-death-of-applicationrb)
* [What's New in Edge Rails: Application.rb Duality is no More](http://archives.ryandaigle.com/articles/2008/11/19/what-s-new-in-edge-rails-application-rb-duality-is-no-more)
### HTTP Digest Authentication Support
@@ -304,7 +304,7 @@ Rails now keeps a per-request local cache of read from the remote cache stores,
Rails can now provide localized views, depending on the locale that you have set. For example, suppose you have a `Posts` controller with a `show` action. By default, this will render `app/views/posts/show.html.erb`. But if you set `I18n.locale = :da`, it will render `app/views/posts/show.da.html.erb`. If the localized template isn't present, the undecorated version will be used. Rails also includes `I18n#available_locales` and `I18n::SimpleBackend#available_locales`, which return an array of the translations that are available in the current Rails project.
-In addition, you can use the same scheme to localize the rescue files in the `public` directory: `public/500.da.html` or `public/404.en.html` work, for example.
+In addition, you can use the same scheme to localize the rescue files in the public directory: `public/500.da.html` or `public/404.en.html` work, for example.
### Partial Scoping for Translations
@@ -468,7 +468,7 @@ options_from_collection_for_select(@product.sizes, :name, :id, :disabled => lamb
```
* Lead Contributor: [Tekin Suleyman](http://tekin.co.uk/)
-* More Information: [New in rails 2.3 - disabled option tags and lambdas for selecting and disabling options from collections](http://tekin.co.uk/2009/03/new-in-rails-23-disabled-option-tags-and-lambdas-for-selecting-and-disabling-options-from-collections/)
+* More Information: [New in rails 2.3 - disabled option tags and lambdas for selecting and disabling options from collections](https://tekin.co.uk/2009/03/new-in-rails-23-disabled-option-tags-and-lambdas-for-selecting-and-disabling-options-from-collections)
### A Note About Template Loading
@@ -533,7 +533,7 @@ If you look up the spec on the "json.org" site, you'll discover that all keys in
### Other Active Support Changes
* You can use `Enumerable#none?` to check that none of the elements match the supplied block.
-* If you're using Active Support [delegates](http://afreshcup.com/2008/10/19/coming-in-rails-22-delegate-prefixes/) the new `:allow_nil` option lets you return `nil` instead of raising an exception when the target object is nil.
+* If you're using Active Support [delegates](https://afreshcup.com/home/2008/10/19/coming-in-rails-22-delegate-prefixes) the new `:allow_nil` option lets you return `nil` instead of raising an exception when the target object is nil.
* `ActiveSupport::OrderedHash`: now implements `each_key` and `each_value`.
* `ActiveSupport::MessageEncryptor` provides a simple way to encrypt information for storage in an untrusted location (like cookies).
* Active Support's `from_xml` no longer depends on XmlSimple. Instead, Rails now includes its own XmlMini implementation, with just the functionality that it requires. This lets Rails dispense with the bundled copy of XmlSimple that it's been carting around.
@@ -592,7 +592,7 @@ The internals of the various <code>rake gem</code> tasks have been substantially
* Internal Rails testing has been switched from `Test::Unit::TestCase` to `ActiveSupport::TestCase`, and the Rails core requires Mocha to test.
* The default `environment.rb` file has been decluttered.
* The dbconsole script now lets you use an all-numeric password without crashing.
-* `Rails.root` now returns a `Pathname` object, which means you can use it directly with the `join` method to [clean up existing code](http://afreshcup.com/2008/12/05/a-little-rails_root-tidiness/) that uses `File.join`.
+* `Rails.root` now returns a `Pathname` object, which means you can use it directly with the `join` method to [clean up existing code](https://afreshcup.wordpress.com/2008/12/05/a-little-rails_root-tidiness/) that uses `File.join`.
* Various files in /public that deal with CGI and FCGI dispatching are no longer generated in every Rails application by default (you can still get them if you need them by adding `--with-dispatchers` when you run the `rails` command, or add them later with `rake rails:update:generate_dispatchers`).
* Rails Guides have been converted from AsciiDoc to Textile markup.
* Scaffolded views and controllers have been cleaned up a bit.
@@ -605,7 +605,7 @@ Deprecated
A few pieces of older code are deprecated in this release:
-* If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](https://github.com/rails/irs_process_scripts/tree) plugin.
+* If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](https://github.com/rails/irs_process_scripts) plugin.
* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](https://github.com/rails/render_component/tree/master).
* Support for Rails components has been removed.
* If you were one of the people who got used to running `script/performance/request` to look at performance based on integration tests, you need to learn a new trick: that script has been removed from core Rails now. There's a new request_profiler plugin that you can install to get the exact same functionality back.
diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md
index f0e2cb3b63..7ffa7d4a5c 100644
--- a/guides/source/3_0_release_notes.md
+++ b/guides/source/3_0_release_notes.md
@@ -213,7 +213,7 @@ Railties now deprecates:
More information:
* [Discovering Rails 3 generators](http://blog.plataformatec.com.br/2010/01/discovering-rails-3-generators)
-* [The Rails Module (in Rails 3)](http://litanyagainstfear.com/blog/2010/02/03/the-rails-module/)
+* [The Rails Module (in Rails 3)](http://quaran.to/blog/2010/02/03/the-rails-module/)
Action Pack
-----------
diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md
index 6570b19f97..ae6eb27f35 100644
--- a/guides/source/3_2_release_notes.md
+++ b/guides/source/3_2_release_notes.md
@@ -30,13 +30,13 @@ TIP: Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails.
### What to update in your apps
-* Update your Gemfile to depend on
+* Update your `Gemfile` to depend on
* `rails = 3.2.0`
* `sass-rails ~> 3.2.3`
* `coffee-rails ~> 3.2.1`
* `uglifier >= 1.0.3`
-* Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. You can start replacing these plugins by extracting them as gems and adding them in your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
+* Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. You can start replacing these plugins by extracting them as gems and adding them in your `Gemfile`. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
* There are a couple of new configuration changes you'd want to add in `config/environments/development.rb`:
@@ -156,7 +156,7 @@ Railties
will create indexes for `title` and `author` with the latter being a unique index. Some types such as decimal accept custom options. In the example, `price` will be a decimal column with precision and scale set to 7 and 2 respectively.
-* Turn gem has been removed from default Gemfile.
+* Turn gem has been removed from default `Gemfile`.
* Remove old plugin generator `rails generate plugin` in favor of `rails plugin new` command.
diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md
index 6f1b75a42b..a1a6a225b2 100644
--- a/guides/source/4_0_release_notes.md
+++ b/guides/source/4_0_release_notes.md
@@ -55,7 +55,7 @@ $ ruby /path/to/rails/railties/bin/rails new myapp --dev
Major Features
--------------
-[![Rails 4.0](images/rails4_features.png)](http://guides.rubyonrails.org/images/rails4_features.png)
+[![Rails 4.0](images/4_0_release_notes/rails4_features.png)](http://guides.rubyonrails.org/images/4_0_release_notes/rails4_features.png)
### Upgrade
@@ -66,7 +66,7 @@ Major Features
* **ActiveRecord session store** ([commit](https://github.com/rails/rails/commit/0ffe19056c8e8b2f9ae9d487b896cad2ce9387ad)) - The ActiveRecord session store is extracted to a separate gem. Storing sessions in SQL is costly. Instead, use cookie sessions, memcache sessions, or a custom session store.
* **ActiveModel mass assignment protection** ([commit](https://github.com/rails/rails/commit/f8c9a4d3e88181cee644f91e1342bfe896ca64c6)) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters.
* **ActiveResource** ([commit](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d)) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used.
-* **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a Gemfile to manage installed gems.
+* **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a `Gemfile` to manage installed gems.
### ActionPack
diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md
index 6bf65757ec..2c5e665e33 100644
--- a/guides/source/4_1_release_notes.md
+++ b/guides/source/4_1_release_notes.md
@@ -274,7 +274,7 @@ for detailed changes.
* The [Spring application
preloader](https://github.com/rails/spring) is now installed
by default for new applications. It uses the development group of
- the Gemfile, so will not be installed in
+ the `Gemfile`, so will not be installed in
production. ([Pull Request](https://github.com/rails/rails/pull/12958))
* `BACKTRACE` environment variable to show unfiltered backtraces for test
diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md
index a30bfc458a..7105df5634 100644
--- a/guides/source/4_2_release_notes.md
+++ b/guides/source/4_2_release_notes.md
@@ -179,7 +179,7 @@ change your code to use the explicit form (`render file: "foo/bar"`) instead.
`respond_with` and the corresponding class-level `respond_to` have been moved
to the [responders](https://github.com/plataformatec/responders) gem. Add
-`gem 'responders', '~> 2.0'` to your Gemfile to use it:
+`gem 'responders', '~> 2.0'` to your `Gemfile` to use it:
```ruby
# app/controllers/users_controller.rb
@@ -368,7 +368,7 @@ Please refer to the [Changelog][railties] for detailed changes.
### Notable changes
-* Introduced `web-console` in the default application Gemfile.
+* Introduced `web-console` in the default application `Gemfile`.
([Pull Request](https://github.com/rails/rails/pull/11667))
* Added a `required` option to the model generator for associations.
diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md
index 3805fd2a63..656838c6b8 100644
--- a/guides/source/5_0_release_notes.md
+++ b/guides/source/5_0_release_notes.md
@@ -775,7 +775,7 @@ Please refer to the [Changelog][active-record] for detailed changes.
* Added prepared statements support to `mysql2` adapter, for mysql2 0.4.4+,
Previously this was only supported on the deprecated `mysql` legacy adapter.
- To enable, set `prepared_statements: true` in config/database.yml.
+ To enable, set `prepared_statements: true` in `config/database.yml`.
([Pull Request](https://github.com/rails/rails/pull/23461))
* Added ability to call `ActionRecord::Relation#update` on relation objects
diff --git a/guides/source/5_1_release_notes.md b/guides/source/5_1_release_notes.md
index 80c9da6446..852d04b1f6 100644
--- a/guides/source/5_1_release_notes.md
+++ b/guides/source/5_1_release_notes.md
@@ -350,6 +350,13 @@ Please refer to the [Changelog][action-pack] for detailed changes.
* Removed deprecated methods related to controller filters.
([Commit](https://github.com/rails/rails/commit/d7be30e8babf5e37a891522869e7b0191b79b757))
+
+* Removed deprecated support to `:text` and `:nothing` in `render`.
+ ([Commit](https://github.com/rails/rails/commit/79a5ea9eadb4d43b62afacedc0706cbe88c54496),
+ [Commit](https://github.com/rails/rails/commit/57e1c99a280bdc1b324936a690350320a1cd8111))
+
+* Removed deprecated support for calling `HashWithIndifferentAccess` methods on `ActionController::Parameters`.
+ ([Commit](https://github.com/rails/rails/pull/26746/commits/7093ceb480ad6a0a91b511832dad4c6a86981b93))
### Deprecations
diff --git a/guides/source/5_2_release_notes.md b/guides/source/5_2_release_notes.md
new file mode 100644
index 0000000000..ab24c7e590
--- /dev/null
+++ b/guides/source/5_2_release_notes.md
@@ -0,0 +1,861 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+
+Ruby on Rails 5.2 Release Notes
+===============================
+
+Highlights in Rails 5.2:
+
+* Active Storage
+* Redis Cache Store
+* HTTP/2 Early Hints
+* Credentials
+* Content Security Policy
+
+These release notes cover only the major changes. To learn about various bug
+fixes and changes, please refer to the change logs or check out the [list of
+commits](https://github.com/rails/rails/commits/5-2-stable) in the main Rails
+repository on GitHub.
+
+--------------------------------------------------------------------------------
+
+Upgrading to Rails 5.2
+----------------------
+
+If you're upgrading an existing application, it's a great idea to have good test
+coverage before going in. You should also first upgrade to Rails 5.1 in case you
+haven't and make sure your application still runs as expected before attempting
+an update to Rails 5.2. A list of things to watch out for when upgrading is
+available in the
+[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-5-1-to-rails-5-2)
+guide.
+
+Major Features
+--------------
+
+### Active Storage
+
+[Pull Request](https://github.com/rails/rails/pull/30020)
+
+[Active Storage](https://github.com/rails/rails/tree/5-2-stable/activestorage)
+facilitates uploading files to a cloud storage service like
+Amazon S3, Google Cloud Storage, or Microsoft Azure Storage and attaching
+those files to Active Record objects. It comes with a local disk-based service
+for development and testing and supports mirroring files to subordinate
+services for backups and migrations.
+You can read more about Active Storage in the
+[Active Storage Overview](active_storage_overview.html) guide.
+
+### Redis Cache Store
+
+[Pull Request](https://github.com/rails/rails/pull/31134)
+
+Rails 5.2 ships with built-in Redis cache store.
+You can read more about this in the
+[Caching with Rails: An Overview](caching_with_rails.html#activesupport-cache-rediscachestore)
+guide.
+
+### HTTP/2 Early Hints
+
+[Pull Request](https://github.com/rails/rails/pull/30744)
+
+Rails 5.2 supports [HTTP/2 Early Hints](https://tools.ietf.org/html/rfc8297).
+To start the server with Early Hints enabled pass `--early-hints`
+to `bin/rails server`.
+
+### Credentials
+
+[Pull Request](https://github.com/rails/rails/pull/30067)
+
+Added `config/credentials.yml.enc` file to store production app secrets.
+It allows saving any authentication credentials for third-party services
+directly in repository encrypted with a key in the `config/master.key` file or
+the `RAILS_MASTER_KEY` environment variable.
+This will eventually replace `Rails.application.secrets` and the encrypted
+secrets introduced in Rails 5.1.
+Furthermore, Rails 5.2
+[opens API underlying Credentials](https://github.com/rails/rails/pull/30940),
+so you can easily deal with other encrypted configurations, keys, and files.
+You can read more about this in the
+[Securing Rails Applications](security.html#custom-credentials)
+guide.
+
+### Content Security Policy
+
+[Pull Request](https://github.com/rails/rails/pull/31162)
+
+Rails 5.2 ships with a new DSL that allows you to configure a
+[Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)
+for your application. You can configure a global default policy and then
+override it on a per-resource basis and even use lambdas to inject per-request
+values into the header such as account subdomains in a multi-tenant application.
+You can read more about this in the
+[Securing Rails Applications](security.html#content-security-policy)
+guide.
+
+Railties
+--------
+
+Please refer to the [Changelog][railties] for detailed changes.
+
+### Deprecations
+
+* Deprecate `capify!` method in generators and templates.
+ ([Pull Request](https://github.com/rails/rails/pull/29493))
+
+* Passing the environment's name as a regular argument to the
+ `rails dbconsole` and `rails console` commands is deprecated.
+ The `-e` option should be used instead.
+ ([Commit](https://github.com/rails/rails/commit/48b249927375465a7102acc71c2dfb8d49af8309))
+
+* Deprecate using subclass of `Rails::Application` to start the Rails server.
+ ([Pull Request](https://github.com/rails/rails/pull/30127))
+
+* Deprecate `after_bundle` callback in Rails plugin templates.
+ ([Pull Request](https://github.com/rails/rails/pull/29446))
+
+### Notable changes
+
+* Added a shared section to `config/database.yml` that will be loaded for
+ all environments.
+ ([Pull Request](https://github.com/rails/rails/pull/28896))
+
+* Add `railtie.rb` to the plugin generator.
+ ([Pull Request](https://github.com/rails/rails/pull/29576))
+
+* Clear screenshot files in `tmp:clear` task.
+ ([Pull Request](https://github.com/rails/rails/pull/29534))
+
+* Skip unused components when running `bin/rails app:update`.
+ If the initial app generation skipped Action Cable, Active Record etc.,
+ the update task honors those skips too.
+ ([Pull Request](https://github.com/rails/rails/pull/29645))
+
+* Allow passing a custom connection name to the `rails dbconsole`
+ command when using a 3-level database configuration.
+ Example: `bin/rails dbconsole -c replica`.
+ ([Commit](https://github.com/rails/rails/commit/1acd9a6464668d4d54ab30d016829f60b70dbbeb))
+
+* Properly expand shortcuts for environment's name running the `console`
+ and `dbconsole` commands.
+ ([Commit](https://github.com/rails/rails/commit/3777701f1380f3814bd5313b225586dec64d4104))
+
+* Add `bootsnap` to default `Gemfile`.
+ ([Pull Request](https://github.com/rails/rails/pull/29313))
+
+* Support `-` as a platform-agnostic way to run a script from stdin with
+ `rails runner`
+ ([Pull Request](https://github.com/rails/rails/pull/26343))
+
+* Add `ruby x.x.x` version to `Gemfile` and create `.ruby-version`
+ root file containing the current Ruby version when new Rails applications
+ are created.
+ ([Pull Request](https://github.com/rails/rails/pull/30016))
+
+* Add `--skip-action-cable` option to the plugin generator.
+ ([Pull Request](https://github.com/rails/rails/pull/30164))
+
+* Add `git_source` to `Gemfile` for plugin generator.
+ ([Pull Request](https://github.com/rails/rails/pull/30110))
+
+* Skip unused components when running `bin/rails` in Rails plugin.
+ ([Commit](https://github.com/rails/rails/commit/62499cb6e088c3bc32a9396322c7473a17a28640))
+
+* Optimize indentation for generator actions.
+ ([Pull Request](https://github.com/rails/rails/pull/30166))
+
+* Optimize routes indentation.
+ ([Pull Request](https://github.com/rails/rails/pull/30241))
+
+* Add `--skip-yarn` option to the plugin generator.
+ ([Pull Request](https://github.com/rails/rails/pull/30238))
+
+* Support multiple versions arguments for `gem` method of Generators.
+ ([Pull Request](https://github.com/rails/rails/pull/30323))
+
+* Derive `secret_key_base` from the app name in development and test
+ environments.
+ ([Pull Request](https://github.com/rails/rails/pull/30067))
+
+* Add `mini_magick` to default `Gemfile` as comment.
+ ([Pull Request](https://github.com/rails/rails/pull/30633))
+
+* `rails new` and `rails plugin new` get `Active Storage` by default.
+ Add ability to skip `Active Storage` with `--skip-active-storage`
+ and do so automatically when `--skip-active-record` is used.
+ ([Pull Request](https://github.com/rails/rails/pull/30101))
+
+Action Cable
+------------
+
+Please refer to the [Changelog][action-cable] for detailed changes.
+
+### Removals
+
+* Removed deprecated evented redis adapter.
+ ([Commit](https://github.com/rails/rails/commit/48766e32d31651606b9f68a16015ad05c3b0de2c))
+
+### Notable changes
+
+* Add support for `host`, `port`, `db` and `password` options in cable.yml
+ ([Pull Request](https://github.com/rails/rails/pull/29528))
+
+* Hash long stream identifiers when using PostgreSQL adapter.
+ ([Pull Request](https://github.com/rails/rails/pull/29297))
+
+Action Pack
+-----------
+
+Please refer to the [Changelog][action-pack] for detailed changes.
+
+### Removals
+
+* Remove deprecated `ActionController::ParamsParser::ParseError`.
+ ([Commit](https://github.com/rails/rails/commit/e16c765ac6dcff068ff2e5554d69ff345c003de1))
+
+### Deprecations
+
+* Deprecate `#success?`, `#missing?` and `#error?` aliases of
+ `ActionDispatch::TestResponse`.
+ ([Pull Request](https://github.com/rails/rails/pull/30104))
+
+### Notable changes
+
+* Add support for recyclable cache keys with fragment caching.
+ ([Pull Request](https://github.com/rails/rails/pull/29092))
+
+* Change the cache key format for fragments to make it easier to debug key
+ churn.
+ ([Pull Request](https://github.com/rails/rails/pull/29092))
+
+* AEAD encrypted cookies and sessions with GCM.
+ ([Pull Request](https://github.com/rails/rails/pull/28132))
+
+* Protect from forgery by default.
+ ([Pull Request](https://github.com/rails/rails/pull/29742))
+
+* Enforce signed/encrypted cookie expiry server side.
+ ([Pull Request](https://github.com/rails/rails/pull/30121))
+
+* Cookies `:expires` option supports `ActiveSupport::Duration` object.
+ ([Pull Request](https://github.com/rails/rails/pull/30121))
+
+* Use Capybara registered `:puma` server config.
+ ([Pull Request](https://github.com/rails/rails/pull/30638))
+
+* Simplify cookies middleware with key rotation support.
+ ([Pull Request](https://github.com/rails/rails/pull/29716))
+
+* Add ability to enable Early Hints for HTTP/2.
+ ([Pull Request](https://github.com/rails/rails/pull/30744))
+
+* Add headless chrome support to System Tests.
+ ([Pull Request](https://github.com/rails/rails/pull/30876))
+
+* Add `:allow_other_host` option to `redirect_back` method.
+ ([Pull Request](https://github.com/rails/rails/pull/30850))
+
+* Make `assert_recognizes` to traverse mounted engines.
+ ([Pull Request](https://github.com/rails/rails/pull/22435))
+
+* Add DSL for configuring Content-Security-Policy header.
+ ([Pull Request](https://github.com/rails/rails/pull/31162),
+ [Commit](https://github.com/rails/rails/commit/619b1b6353a65e1635d10b8f8c6630723a5a6f1a),
+ [Commit](https://github.com/rails/rails/commit/4ec8bf68ff92f35e79232fbd605012ce1f4e1e6e))
+
+* Register most popular audio/video/font mime types supported by modern
+ browsers.
+ ([Pull Request](https://github.com/rails/rails/pull/31251))
+
+* Changed the default system test screenshot output from `inline` to `simple`.
+ ([Commit](https://github.com/rails/rails/commit/9d6e288ee96d6241f864dbf90211c37b14a57632))
+
+* Add headless firefox support to System Tests.
+ ([Pull Request](https://github.com/rails/rails/pull/31365))
+
+* Add secure `X-Download-Options` and `X-Permitted-Cross-Domain-Policies` to
+ default headers set.
+ ([Commit](https://github.com/rails/rails/commit/5d7b70f4336d42eabfc403e9f6efceb88b3eff44))
+
+* Changed the system tests to set Puma as default server only when the
+ user haven't specified manually another server.
+ ([Pull Request](https://github.com/rails/rails/pull/31384))
+
+* Add `Referrer-Policy` header to default headers set.
+ ([Commit](https://github.com/rails/rails/commit/428939be9f954d39b0c41bc53d85d0d106b9d1a1))
+
+* Matches behavior of `Hash#each` in `ActionController::Parameters#each`.
+ ([Pull Request](https://github.com/rails/rails/pull/27790))
+
+* Add support for automatic nonce generation for Rails UJS.
+ ([Commit](https://github.com/rails/rails/commit/b2f0a8945956cd92dec71ec4e44715d764990a49))
+
+* Update the default HSTS max-age value to 31536000 seconds (1 year)
+ to meet the minimum max-age requirement for https://hstspreload.org/.
+ ([Commit](https://github.com/rails/rails/commit/30b5f469a1d30c60d1fb0605e84c50568ff7ed37))
+
+* Add alias method `to_hash` to `to_h` for `cookies`.
+ Add alias method `to_h` to `to_hash` for `session`.
+ ([Commit](https://github.com/rails/rails/commit/50a62499e41dfffc2903d468e8b47acebaf9b500))
+
+Action View
+-----------
+
+Please refer to the [Changelog][action-view] for detailed changes.
+
+### Removals
+
+* Remove deprecated Erubis ERB handler.
+ ([Commit](https://github.com/rails/rails/commit/7de7f12fd140a60134defe7dc55b5a20b2372d06))
+
+### Deprecations
+
+* Deprecate `image_alt` helper which used to add default alt text to
+ the images generated by `image_tag`.
+ ([Pull Request](https://github.com/rails/rails/pull/30213))
+
+### Notable changes
+
+* Add `:json` type to `auto_discovery_link_tag` to support
+ [JSON Feeds](https://jsonfeed.org/version/1).
+ ([Pull Request](https://github.com/rails/rails/pull/29158))
+
+* Add `srcset` option to `image_tag` helper.
+ ([Pull Request](https://github.com/rails/rails/pull/29349))
+
+* Fix issues with `field_error_proc` wrapping `optgroup` and
+ select divider `option`.
+ ([Pull Request](https://github.com/rails/rails/pull/31088))
+
+* Change `form_with` to generates ids by default.
+ ([Commit](https://github.com/rails/rails/commit/260d6f112a0ffdbe03e6f5051504cb441c1e94cd))
+
+* Add `preload_link_tag` helper.
+ ([Pull Request](https://github.com/rails/rails/pull/31251))
+
+* Allow the use of callable objects as group methods for grouped selects.
+ ([Pull Request](https://github.com/rails/rails/pull/31578))
+
+Action Mailer
+-------------
+
+Please refer to the [Changelog][action-mailer] for detailed changes.
+
+### Notable changes
+
+* Allow Action Mailer classes to configure their delivery job.
+ ([Pull Request](https://github.com/rails/rails/pull/29457))
+
+* Add `assert_enqueued_email_with` test helper.
+ ([Pull Request](https://github.com/rails/rails/pull/30695))
+
+Active Record
+-------------
+
+Please refer to the [Changelog][active-record] for detailed changes.
+
+### Removals
+
+* Remove deprecated `#migration_keys`.
+ ([Pull Request](https://github.com/rails/rails/pull/30337))
+
+* Remove deprecated support to `quoted_id` when typecasting
+ an Active Record object.
+ ([Commit](https://github.com/rails/rails/commit/82472b3922bda2f337a79cef961b4760d04f9689))
+
+* Remove deprecated argument `default` from `index_name_exists?`.
+ ([Commit](https://github.com/rails/rails/commit/8f5b34df81175e30f68879479243fbce966122d7))
+
+* Remove deprecated support to passing a class to `:class_name`
+ on associations.
+ ([Commit](https://github.com/rails/rails/commit/e65aff70696be52b46ebe57207ebd8bb2cfcdbb6))
+
+* Remove deprecated methods `initialize_schema_migrations_table` and
+ `initialize_internal_metadata_table`.
+ ([Commit](https://github.com/rails/rails/commit/c9660b5777707658c414b430753029cd9bc39934))
+
+* Remove deprecated method `supports_migrations?`.
+ ([Commit](https://github.com/rails/rails/commit/9438c144b1893f2a59ec0924afe4d46bd8d5ffdd))
+
+* Remove deprecated method `supports_primary_key?`.
+ ([Commit](https://github.com/rails/rails/commit/c56ff22fc6e97df4656ddc22909d9bf8b0c2cbb1))
+
+* Remove deprecated method
+ `ActiveRecord::Migrator.schema_migrations_table_name`.
+ ([Commit](https://github.com/rails/rails/commit/7df6e3f3cbdea9a0460ddbab445c81fbb1cfd012))
+
+* Remove deprecated argument `name` from `#indexes`.
+ ([Commit](https://github.com/rails/rails/commit/d6b779ecebe57f6629352c34bfd6c442ac8fba0e))
+
+* Remove deprecated arguments from `#verify!`.
+ ([Commit](https://github.com/rails/rails/commit/9c6ee1bed0292fc32c23dc1c68951ae64fc510be))
+
+* Remove deprecated configuration `.error_on_ignored_order_or_limit`.
+ ([Commit](https://github.com/rails/rails/commit/e1066f450d1a99c9a0b4d786b202e2ca82a4c3b3))
+
+* Remove deprecated method `#scope_chain`.
+ ([Commit](https://github.com/rails/rails/commit/ef7784752c5c5efbe23f62d2bbcc62d4fd8aacab))
+
+* Remove deprecated method `#sanitize_conditions`.
+ ([Commit](https://github.com/rails/rails/commit/8f5413b896099f80ef46a97819fe47a820417bc2))
+
+### Deprecations
+
+* Deprecate `supports_statement_cache?`.
+ ([Pull Request](https://github.com/rails/rails/pull/28938))
+
+* Deprecate passing arguments and block at the same time to
+ `count` and `sum` in `ActiveRecord::Calculations`.
+ ([Pull Request](https://github.com/rails/rails/pull/29262))
+
+* Deprecate delegating to `arel` in `Relation`.
+ ([Pull Request](https://github.com/rails/rails/pull/29619))
+
+* Deprecate `set_state` method in `TransactionState`.
+ ([Commit](https://github.com/rails/rails/commit/608ebccf8f6314c945444b400a37c2d07f21b253))
+
+* Deprecate `expand_hash_conditions_for_aggregates` without replacement.
+ ([Commit](https://github.com/rails/rails/commit/7ae26885d96daee3809d0bd50b1a440c2f5ffb69))
+
+### Notable changes
+
+* When calling the dynamic fixture accessor method with no arguments, it now
+ returns all fixtures of this type. Previously this method always returned
+ an empty array.
+ ([Pull Request](https://github.com/rails/rails/pull/28692))
+
+* Fix inconsistency with changed attributes when overriding
+ Active Record attribute reader.
+ ([Pull Request](https://github.com/rails/rails/pull/28661))
+
+* Support Descending Indexes for MySQL.
+ ([Pull Request](https://github.com/rails/rails/pull/28773))
+
+* Fix `bin/rails db:forward` first migration.
+ ([Commit](https://github.com/rails/rails/commit/b77d2aa0c336492ba33cbfade4964ba0eda3ef84))
+
+* Raise error `UnknownMigrationVersionError` on the movement of migrations
+ when the current migration does not exist.
+ ([Commit](https://github.com/rails/rails/commit/bb9d6eb094f29bb94ef1f26aa44f145f17b973fe))
+
+* Respect `SchemaDumper.ignore_tables` in rake tasks for
+ databases structure dump.
+ ([Pull Request](https://github.com/rails/rails/pull/29077))
+
+* Add `ActiveRecord::Base#cache_version` to support recyclable cache keys via
+ the new versioned entries in `ActiveSupport::Cache`. This also means that
+ `ActiveRecord::Base#cache_key` will now return a stable key that
+ does not include a timestamp any more.
+ ([Pull Request](https://github.com/rails/rails/pull/29092))
+
+* Prevent creation of bind param if casted value is nil.
+ ([Pull Request](https://github.com/rails/rails/pull/29282))
+
+* Use bulk INSERT to insert fixtures for better performance.
+ ([Pull Request](https://github.com/rails/rails/pull/29504))
+
+* Merging two relations representing nested joins no longer transforms
+ the joins of the merged relation into LEFT OUTER JOIN.
+ ([Pull Request](https://github.com/rails/rails/pull/27063))
+
+* Fix transactions to apply state to child transactions.
+ Previously, if you had a nested transaction and the outer transaction was
+ rolledback, the record from the inner transaction would still be marked
+ as persisted. It was fixed by applying the state of the parent
+ transaction to the child transaction when the parent transaction is
+ rolledback. This will correctly mark records from the inner transaction
+ as not persisted.
+ ([Commit](https://github.com/rails/rails/commit/0237da287eb4c507d10a0c6d94150093acc52b03))
+
+* Fix eager loading/preloading association with scope including joins.
+ ([Pull Request](https://github.com/rails/rails/pull/29413))
+
+* Prevent errors raised by `sql.active_record` notification subscribers
+ from being converted into `ActiveRecord::StatementInvalid` exceptions.
+ ([Pull Request](https://github.com/rails/rails/pull/29692))
+
+* Skip query caching when working with batches of records
+ (`find_each`, `find_in_batches`, `in_batches`).
+ ([Commit](https://github.com/rails/rails/commit/b83852e6eed5789b23b13bac40228e87e8822b4d))
+
+* Change sqlite3 boolean serialization to use 1 and 0.
+ SQLite natively recognizes 1 and 0 as true and false, but does not natively
+ recognize 't' and 'f' as was previously serialized.
+ ([Pull Request](https://github.com/rails/rails/pull/29699))
+
+* Values constructed using multi-parameter assignment will now use the
+ post-type-cast value for rendering in single-field form inputs.
+ ([Commit](https://github.com/rails/rails/commit/1519e976b224871c7f7dd476351930d5d0d7faf6))
+
+* `ApplicationRecord` is no longer generated when generating models. If you
+ need to generate it, it can be created with `rails g application_record`.
+ ([Pull Request](https://github.com/rails/rails/pull/29916))
+
+* `Relation#or` now accepts two relations who have different values for
+ `references` only, as `references` can be implicitly called by `where`.
+ ([Commit](https://github.com/rails/rails/commit/ea6139101ccaf8be03b536b1293a9f36bc12f2f7))
+
+* When using `Relation#or`, extract the common conditions and
+ put them before the OR condition.
+ ([Pull Request](https://github.com/rails/rails/pull/29950))
+
+* Add `binary` fixture helper method.
+ ([Pull Request](https://github.com/rails/rails/pull/30073))
+
+* Automatically guess the inverse associations for STI.
+ ([Pull Request](https://github.com/rails/rails/pull/23425))
+
+* Add new error class `LockWaitTimeout` which will be raised
+ when lock wait timeout exceeded.
+ ([Pull Request](https://github.com/rails/rails/pull/30360))
+
+* Update payload names for `sql.active_record` instrumentation to be
+ more descriptive.
+ ([Pull Request](https://github.com/rails/rails/pull/30619))
+
+* Use given algorithm while removing index from database.
+ ([Pull Request](https://github.com/rails/rails/pull/24199))
+
+* Passing a `Set` to `Relation#where` now behaves the same as passing
+ an array.
+ ([Commit](https://github.com/rails/rails/commit/9cf7e3494f5bd34f1382c1ff4ea3d811a4972ae2))
+
+* PostgreSQL `tsrange` now preserves subsecond precision.
+ ([Pull Request](https://github.com/rails/rails/pull/30725))
+
+* Raises when calling `lock!` in a dirty record.
+ ([Commit](https://github.com/rails/rails/commit/63cf15877bae859ff7b4ebaf05186f3ca79c1863))
+
+* Fixed a bug where column orders for an index weren't written to
+ `db/schema.rb` when using the sqlite adapter.
+ ([Pull Request](https://github.com/rails/rails/pull/30970))
+
+* Fix `bin/rails db:migrate` with specified `VERSION`.
+ `bin/rails db:migrate` with empty VERSION behaves as without `VERSION`.
+ Check a format of `VERSION`: Allow a migration version number
+ or name of a migration file. Raise error if format of `VERSION` is invalid.
+ Raise error if target migration doesn't exist.
+ ([Pull Request](https://github.com/rails/rails/pull/30714))
+
+* Add new error class `StatementTimeout` which will be raised
+ when statement timeout exceeded.
+ ([Pull Request](https://github.com/rails/rails/pull/31129))
+
+* `update_all` will now pass its values to `Type#cast` before passing them to
+ `Type#serialize`. This means that `update_all(foo: 'true')` will properly
+ persist a boolean.
+ ([Commit](https://github.com/rails/rails/commit/68fe6b08ee72cc47263e0d2c9ff07f75c4b42761))
+
+* Require raw SQL fragments to be explicitly marked when used in
+ relation query methods.
+ ([Commit](https://github.com/rails/rails/commit/a1ee43d2170dd6adf5a9f390df2b1dde45018a48),
+ [Commit](https://github.com/rails/rails/commit/e4a921a75f8702a7dbaf41e31130fe884dea93f9))
+
+* Add `#up_only` to database migrations for code that is only relevant when
+ migrating up, e.g. populating a new column.
+ ([Pull Request](https://github.com/rails/rails/pull/31082))
+
+* Add new error class `QueryCanceled` which will be raised
+ when canceling statement due to user request.
+ ([Pull Request](https://github.com/rails/rails/pull/31235))
+
+* Don't allow scopes to be defined which conflict with instance methods
+ on `Relation`.
+ ([Pull Request](https://github.com/rails/rails/pull/31179))
+
+* Add support for PostgreSQL operator classes to `add_index`.
+ ([Pull Request](https://github.com/rails/rails/pull/19090))
+
+* Log database query callers.
+ ([Pull Request](https://github.com/rails/rails/pull/26815),
+ [Pull Request](https://github.com/rails/rails/pull/31519),
+ [Pull Request](https://github.com/rails/rails/pull/31690))
+
+* Undefine attribute methods on descendants when resetting column information.
+ ([Pull Request](https://github.com/rails/rails/pull/31475))
+
+* Using subselect for `delete_all` with `limit` or `offset`.
+ ([Commit](https://github.com/rails/rails/commit/9e7260da1bdc0770cf4ac547120c85ab93ff3d48))
+
+* Fixed inconsistency with `first(n)` when used with `limit()`.
+ The `first(n)` finder now respects the `limit()`, making it consistent
+ with `relation.to_a.first(n)`, and also with the behavior of `last(n)`.
+ ([Pull Request](https://github.com/rails/rails/pull/27597))
+
+* Fix nested `has_many :through` associations on unpersisted parent instances.
+ ([Commit](https://github.com/rails/rails/commit/027f865fc8b262d9ba3ee51da3483e94a5489b66))
+
+* Take into account association conditions when deleting through records.
+ ([Commit](https://github.com/rails/rails/commit/ae48c65e411e01c1045056562319666384bb1b63))
+
+* Don't allow destroyed object mutation after `save` or `save!` is called.
+ ([Commit](https://github.com/rails/rails/commit/562dd0494a90d9d47849f052e8913f0050f3e494))
+
+* Fix relation merger issue with `left_outer_joins`.
+ ([Pull Request](https://github.com/rails/rails/pull/27860))
+
+* Support for PostgreSQL foreign tables.
+ ([Pull Request](https://github.com/rails/rails/pull/31549))
+
+* Clear the transaction state when an Active Record object is duped.
+ ([Pull Request](https://github.com/rails/rails/pull/31751))
+
+* Fix not expanded problem when passing an Array object as argument
+ to the where method using `composed_of` column.
+ ([Pull Request](https://github.com/rails/rails/pull/31724))
+
+* Make `reflection.klass` raise if `polymorphic?` not to be misused.
+ ([Commit](https://github.com/rails/rails/commit/63fc1100ce054e3e11c04a547cdb9387cd79571a))
+
+* Fix `#columns_for_distinct` of MySQL and PostgreSQL to make
+ `ActiveRecord::FinderMethods#limited_ids_for` use correct primary key values
+ even if `ORDER BY` columns include other table's primary key.
+ ([Commit](https://github.com/rails/rails/commit/851618c15750979a75635530200665b543561a44))
+
+* Fix `dependent: :destroy` issue for has_one/belongs_to relationship where
+ the parent class was getting deleted when the child was not.
+ ([Commit](https://github.com/rails/rails/commit/b0fc04aa3af338d5a90608bf37248668d59fc881))
+
+Active Model
+------------
+
+Please refer to the [Changelog][active-model] for detailed changes.
+
+### Notable changes
+
+* Fix methods `#keys`, `#values` in `ActiveModel::Errors`.
+ Change `#keys` to only return the keys that don't have empty messages.
+ Change `#values` to only return the not empty values.
+ ([Pull Request](https://github.com/rails/rails/pull/28584))
+
+* Add method `#merge!` for `ActiveModel::Errors`.
+ ([Pull Request](https://github.com/rails/rails/pull/29714))
+
+* Allow passing a Proc or Symbol to length validator options.
+ ([Pull Request](https://github.com/rails/rails/pull/30674))
+
+* Execute `ConfirmationValidator` validation when `_confirmation`'s value
+ is `false`.
+ ([Pull Request](https://github.com/rails/rails/pull/31058))
+
+* Models using the attributes API with a proc default can now be marshalled.
+ ([Commit](https://github.com/rails/rails/commit/0af36c62a5710e023402e37b019ad9982e69de4b))
+
+* Do not lose all multiple `:includes` with options in serialization.
+ ([Commit](https://github.com/rails/rails/commit/853054bcc7a043eea78c97e7705a46abb603cc44))
+
+Active Support
+--------------
+
+Please refer to the [Changelog][active-support] for detailed changes.
+
+### Removals
+
+* Remove deprecated `:if` and `:unless` string filter for callbacks.
+ ([Commit](https://github.com/rails/rails/commit/c792354adcbf8c966f274915c605c6713b840548))
+
+* Remove deprecated `halt_callback_chains_on_return_false` option.
+ ([Commit](https://github.com/rails/rails/commit/19fbbebb1665e482d76cae30166b46e74ceafe29))
+
+### Deprecations
+
+* Deprecate `Module#reachable?` method.
+ ([Pull Request](https://github.com/rails/rails/pull/30624))
+
+* Deprecate `secrets.secret_token`.
+ ([Commit](https://github.com/rails/rails/commit/fbcc4bfe9a211e219da5d0bb01d894fcdaef0a0e))
+
+### Notable changes
+
+* Add `fetch_values` for `HashWithIndifferentAccess`.
+ ([Pull Request](https://github.com/rails/rails/pull/28316))
+
+* Add support for `:offset` to `Time#change`.
+ ([Commit](https://github.com/rails/rails/commit/851b7f866e13518d900407c78dcd6eb477afad06))
+
+* Add support for `:offset` and `:zone`
+ to `ActiveSupport::TimeWithZone#change`.
+ ([Commit](https://github.com/rails/rails/commit/851b7f866e13518d900407c78dcd6eb477afad06))
+
+* Pass gem name and deprecation horizon to deprecation notifications.
+ ([Pull Request](https://github.com/rails/rails/pull/28800))
+
+* Add support for versioned cache entries. This enables the cache stores to
+ recycle cache keys, greatly saving on storage in cases with frequent churn.
+ Works together with the separation of `#cache_key` and `#cache_version`
+ in Active Record and its use in Action Pack's fragment caching.
+ ([Pull Request](https://github.com/rails/rails/pull/29092))
+
+* Add `ActiveSupport::CurrentAttributes` to provide a thread-isolated
+ attributes singleton. Primary use case is keeping all the per-request
+ attributes easily available to the whole system.
+ ([Pull Request](https://github.com/rails/rails/pull/29180))
+
+* `#singularize` and `#pluralize` now respect uncountables for
+ the specified locale.
+ ([Commit](https://github.com/rails/rails/commit/352865d0f835c24daa9a2e9863dcc9dde9e5371a))
+
+* Add default option to `class_attribute`.
+ ([Pull Request](https://github.com/rails/rails/pull/29270))
+
+* Add `Date#prev_occurring` and `Date#next_occurring` to return
+ specified next/previous occurring day of week.
+ ([Pull Request](https://github.com/rails/rails/pull/26600))
+
+* Add default option to module and class attribute accessors.
+ ([Pull Request](https://github.com/rails/rails/pull/29294))
+
+* Cache: `write_multi`.
+ ([Pull Request](https://github.com/rails/rails/pull/29366))
+
+* Default `ActiveSupport::MessageEncryptor` to use AES 256 GCM encryption.
+ ([Pull Request](https://github.com/rails/rails/pull/29263))
+
+* Add `freeze_time` helper which freezes time to `Time.now` in tests.
+ ([Pull Request](https://github.com/rails/rails/pull/29681))
+
+* Make the order of `Hash#reverse_merge!` consistent
+ with `HashWithIndifferentAccess`.
+ ([Pull Request](https://github.com/rails/rails/pull/28077))
+
+* Add purpose and expiry support to `ActiveSupport::MessageVerifier` and
+ `ActiveSupport::MessageEncryptor`.
+ ([Pull Request](https://github.com/rails/rails/pull/29892))
+
+* Update `String#camelize` to provide feedback when wrong option is passed.
+ ([Pull Request](https://github.com/rails/rails/pull/30039))
+
+* `Module#delegate_missing_to` now raises `DelegationError` if target is nil,
+ similar to `Module#delegate`.
+ ([Pull Request](https://github.com/rails/rails/pull/30191))
+
+* Add `ActiveSupport::EncryptedFile` and
+ `ActiveSupport::EncryptedConfiguration`.
+ ([Pull Request](https://github.com/rails/rails/pull/30067))
+
+* Add `config/credentials.yml.enc` to store production app secrets.
+ ([Pull Request](https://github.com/rails/rails/pull/30067))
+
+* Add key rotation support to `MessageEncryptor` and `MessageVerifier`.
+ ([Pull Request](https://github.com/rails/rails/pull/29716))
+
+* Return an instance of `HashWithIndifferentAccess` from
+ `HashWithIndifferentAccess#transform_keys`.
+ ([Pull Request](https://github.com/rails/rails/pull/30728))
+
+* `Hash#slice` now falls back to Ruby 2.5+'s built-in definition if defined.
+ ([Commit](https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8))
+
+* `IO#to_json` now returns the `to_s` representation, rather than
+ attempting to convert to an array. This fixes a bug where `IO#to_json`
+ would raise an `IOError` when called on an unreadable object.
+ ([Pull Request](https://github.com/rails/rails/pull/30953))
+
+* Add same method signature for `Time#prev_day` and `Time#next_day`
+ in accordance with `Date#prev_day`, `Date#next_day`.
+ Allows pass argument for `Time#prev_day` and `Time#next_day`.
+ ([Commit](https://github.com/rails/rails/commit/61ac2167eff741bffb44aec231f4ea13d004134e))
+
+* Add same method signature for `Time#prev_month` and `Time#next_month`
+ in accordance with `Date#prev_month`, `Date#next_month`.
+ Allows pass argument for `Time#prev_month` and `Time#next_month`.
+ ([Commit](https://github.com/rails/rails/commit/f2c1e3a793570584d9708aaee387214bc3543530))
+
+* Add same method signature for `Time#prev_year` and `Time#next_year`
+ in accordance with `Date#prev_year`, `Date#next_year`.
+ Allows pass argument for `Time#prev_year` and `Time#next_year`.
+ ([Commit](https://github.com/rails/rails/commit/ee9d81837b5eba9d5ec869ae7601d7ffce763e3e))
+
+* Fix acronym support in `humanize`.
+ ([Commit](https://github.com/rails/rails/commit/0ddde0a8fca6a0ca3158e3329713959acd65605d))
+
+* Allow `Range#include?` on TWZ ranges.
+ ([Pull Request](https://github.com/rails/rails/pull/31081))
+
+* Cache: Enable compression by default for values > 1kB.
+ ([Pull Request](https://github.com/rails/rails/pull/31147))
+
+* Redis cache store.
+ ([Pull Request](https://github.com/rails/rails/pull/31134),
+ [Pull Request](https://github.com/rails/rails/pull/31866))
+
+* Handle `TZInfo::AmbiguousTime` errors.
+ ([Pull Request](https://github.com/rails/rails/pull/31128))
+
+* MemCacheStore: Support expiring counters.
+ ([Commit](https://github.com/rails/rails/commit/b22ee64b5b30c6d5039c292235e10b24b1057f6d))
+
+* Make `ActiveSupport::TimeZone.all` return only time zones that are in
+ `ActiveSupport::TimeZone::MAPPING`.
+ ([Pull Request](https://github.com/rails/rails/pull/31176))
+
+* Changed default behaviour of `ActiveSupport::SecurityUtils.secure_compare`,
+ to make it not leak length information even for variable length string.
+ Renamed old `ActiveSupport::SecurityUtils.secure_compare` to
+ `fixed_length_secure_compare`, and started raising `ArgumentError` in
+ case of length mismatch of passed strings.
+ ([Pull Request](https://github.com/rails/rails/pull/24510))
+
+* Use SHA-1 to generate non-sensitive digests, such as the ETag header.
+ ([Pull Request](https://github.com/rails/rails/pull/31289),
+ [Pull Request](https://github.com/rails/rails/pull/31651))
+
+* `assert_changes` will always assert that the expression changes,
+ regardless of `from:` and `to:` argument combinations.
+ ([Pull Request](https://github.com/rails/rails/pull/31011))
+
+* Add missing instrumentation for `read_multi`
+ in `ActiveSupport::Cache::Store`.
+ ([Pull Request](https://github.com/rails/rails/pull/30268))
+
+* Support hash as first argument in `assert_difference`.
+ This allows to specify multiple numeric differences in the same assertion.
+ ([Pull Request](https://github.com/rails/rails/pull/31600))
+
+* Caching: MemCache and Redis `read_multi` and `fetch_multi` speedup.
+ Read from the local in-memory cache before consulting the backend.
+ ([Commit](https://github.com/rails/rails/commit/a2b97e4ffef971607a1be8fc7909f099b6840f36))
+
+Active Job
+----------
+
+Please refer to the [Changelog][active-job] for detailed changes.
+
+### Notable changes
+
+* Allow block to be passed to `ActiveJob::Base.discard_on` to allow custom
+ handling of discard jobs.
+ ([Pull Request](https://github.com/rails/rails/pull/30622))
+
+Ruby on Rails Guides
+--------------------
+
+Please refer to the [Changelog][guides] for detailed changes.
+
+### Notable changes
+
+* Add
+ [Threading and Code Execution in Rails](threading_and_code_execution.html)
+ Guide.
+ ([Pull Request](https://github.com/rails/rails/pull/27494))
+
+* Add [Active Storage Overview](active_storage_overview.html) Guide.
+ ([Pull Request](https://github.com/rails/rails/pull/31037))
+
+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.
+
+[railties]: https://github.com/rails/rails/blob/5-2-stable/railties/CHANGELOG.md
+[action-pack]: https://github.com/rails/rails/blob/5-2-stable/actionpack/CHANGELOG.md
+[action-view]: https://github.com/rails/rails/blob/5-2-stable/actionview/CHANGELOG.md
+[action-mailer]: https://github.com/rails/rails/blob/5-2-stable/actionmailer/CHANGELOG.md
+[action-cable]: https://github.com/rails/rails/blob/5-2-stable/actioncable/CHANGELOG.md
+[active-record]: https://github.com/rails/rails/blob/5-2-stable/activerecord/CHANGELOG.md
+[active-model]: https://github.com/rails/rails/blob/5-2-stable/activemodel/CHANGELOG.md
+[active-support]: https://github.com/rails/rails/blob/5-2-stable/activesupport/CHANGELOG.md
+[active-job]: https://github.com/rails/rails/blob/5-2-stable/activejob/CHANGELOG.md
+[guides]: https://github.com/rails/rails/blob/5-2-stable/guides/CHANGELOG.md
diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb
index 8afec00018..5dd6bfdd23 100644
--- a/guides/source/_welcome.html.erb
+++ b/guides/source/_welcome.html.erb
@@ -10,16 +10,20 @@
</p>
<% else %>
<p>
- These are the new guides for Rails 5.1 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>.
+ These are the new guides for Rails 5.2 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>.
These guides are designed to make you immediately productive with Rails, and to help you understand how all of the pieces fit together.
</p>
<% end %>
<p>
The guides for earlier releases:
+<a href="http://guides.rubyonrails.org/v5.2/">Rails 5.2</a>,
+<a href="http://guides.rubyonrails.org/v5.1/">Rails 5.1</a>,
<a href="http://guides.rubyonrails.org/v5.0/">Rails 5.0</a>,
<a href="http://guides.rubyonrails.org/v4.2/">Rails 4.2</a>,
<a href="http://guides.rubyonrails.org/v4.1/">Rails 4.1</a>,
<a href="http://guides.rubyonrails.org/v4.0/">Rails 4.0</a>,
-<a href="http://guides.rubyonrails.org/v3.2/">Rails 3.2</a>, and
+<a href="http://guides.rubyonrails.org/v3.2/">Rails 3.2</a>,
+<a href="http://guides.rubyonrails.org/v3.1/">Rails 3.1</a>,
+<a href="http://guides.rubyonrails.org/v3.0/">Rails 3.0</a>, and
<a href="http://guides.rubyonrails.org/v2.3/">Rails 2.3</a>.
</p>
diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md
index 31151e0329..c250db2e0c 100644
--- a/guides/source/action_cable_overview.md
+++ b/guides/source/action_cable_overview.md
@@ -1,12 +1,14 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+
Action Cable Overview
=====================
-In this guide you will learn how Action Cable works and how to use WebSockets to
+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:
-* What Action Cable is and its integration on backend and frontend
+* What Action Cable is and its integration backend and frontend
* How to setup Action Cable
* How to setup channels
* Deployment and Architecture setup for running Action Cable
@@ -129,7 +131,7 @@ subscriptions based on an identifier sent by the cable consumer.
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
# Called when the consumer has successfully
- # become a subscriber of this channel.
+ # become a subscriber to this channel.
def subscribed
end
end
@@ -225,7 +227,7 @@ 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
+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.
@@ -515,8 +517,8 @@ user. For a user with an ID of 1, the broadcasting name would be
The channel has been instructed to stream everything that arrives at
`web_notifications:1` directly to the client by invoking the `received`
callback. The data passed as argument 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`.
+to the server-side broadcast call, JSON encoded for the trip across the wire
+and unpacked for the data argument arriving as `received`.
### More Complete Examples
@@ -555,9 +557,8 @@ The async adapter is intended for development/testing and should not be used in
##### Redis Adapter
-Action Cable contains two Redis adapters: "normal" Redis and Evented Redis. Both
-of the adapters require users to provide a URL pointing to the Redis server.
-Additionally, a channel_prefix may be provided to avoid channel name collisions
+The Redis adapter requires users to provide a URL pointing to the Redis server.
+Additionally, a `channel_prefix` may be provided to avoid channel name collisions
when using the same Redis server for multiple applications. See the [Redis PubSub documentation](https://redis.io/topics/pubsub#database-amp-scoping) for more details.
##### PostgreSQL Adapter
@@ -570,7 +571,7 @@ This may change in the future. [#27214](https://github.com/rails/rails/issues/27
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.
+strings or regular expressions, against which a check for the match will be performed.
```ruby
config.action_cable.allowed_request_origins = ['http://rubyonrails.com', %r{http://ruby.*}]
@@ -593,7 +594,7 @@ environment configuration files.
### Other Configurations
-The other common option to configure, is the log tags applied to the
+The other common option to configure is the log tags applied to the
per-connection logger. Here's an example that uses
the user account id if available, else "no-account" while tagging:
@@ -608,7 +609,7 @@ config.action_cable.log_tags = [
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
+Also, note that your server must provide at least the same number of database
connections as you have workers. The default worker pool size is set to 4, so
that means you have to make at least that available. You can change that in
`config/database.yml` through the `pool` attribute.
@@ -669,8 +670,8 @@ authentication. You can see one way of doing that with Devise in this [article](
## 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
+pubsub internals. By default, asynchronous, inline, PostgreSQL, and 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),
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index 2c3f74c3e1..cd685a228e 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -21,7 +21,7 @@ After reading this guide, you will know:
What Does a Controller Do?
--------------------------
-Action Controller is the C in MVC. After the router has determined which controller to use for a request, the controller is responsible for making sense of the request and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible.
+Action Controller is the C in [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). After the router has determined which controller to use for a request, the controller is responsible for making sense of the request, and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible.
For most conventional [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) applications, the controller will receive the request (this is invisible to you as the developer), fetch or save data from a model and use a view to create HTML output. If your controller needs to do things a little differently, that's not a problem, this is just the most common way for a controller to work.
@@ -51,7 +51,7 @@ class ClientsController < ApplicationController
end
```
-As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and call its `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. The `new` method could make available to the view a `@client` instance variable by creating a new `Client`:
+As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and call its `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. By creating a new `Client`, the `new` method can make a `@client` instance variable accessible in the view:
```ruby
def new
@@ -400,9 +400,9 @@ Rails.application.config.session_store :cookie_store, key: '_your_app_session',
Rails sets up (for the CookieStore) a secret key used for signing the session data in `config/credentials.yml.enc`. This can be changed with `bin/rails credentials:edit`.
```ruby
-# amazon:
-# access_key_id: 123
-# secret_access_key: 345
+# aws:
+# access_key_id: 123
+# secret_access_key: 345
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: 492f...
@@ -450,14 +450,16 @@ class LoginsController < ApplicationController
end
```
-To remove something from the session, assign that key to be `nil`:
+To remove something from the session, delete the key/value pair:
```ruby
class LoginsController < ApplicationController
# "Delete" a login, aka "log the user out"
def destroy
# Remove the user id from the session
- @_current_user = session[:current_user_id] = nil
+ session.delete(:current_user_id)
+ # Clear the memoized current user
+ @_current_user = nil
redirect_to root_url
end
end
@@ -476,7 +478,7 @@ Let's use the act of logging out as an example. The controller can send a messag
```ruby
class LoginsController < ApplicationController
def destroy
- session[:current_user_id] = nil
+ session.delete(:current_user_id)
flash[:notice] = "You have successfully logged out."
redirect_to root_url
end
@@ -654,8 +656,8 @@ class UsersController < ApplicationController
@users = User.all
respond_to do |format|
format.html # index.html.erb
- format.xml { render xml: @users}
- format.json { render json: @users}
+ format.xml { render xml: @users }
+ format.json { render json: @users }
end
end
end
@@ -784,9 +786,9 @@ The way this is done is to add a non-guessable token which is only known to your
If you generate a form like this:
```erb
-<%= form_for @user do |f| %>
- <%= f.text_field :username %>
- <%= f.text_field :password %>
+<%= form_with model: @user, local: true do |form| %>
+ <%= form.text_field :username %>
+ <%= form.text_field :password %>
<% end %>
```
@@ -855,7 +857,7 @@ If you want to set custom headers for a response then `response.headers` is the
response.headers["Content-Type"] = "application/pdf"
```
-Note: in the above case it would make more sense to use the `content_type` setter directly.
+NOTE: In the above case it would make more sense to use the `content_type` setter directly.
HTTP Authentications
--------------------
@@ -1116,7 +1118,7 @@ Rails default exception handling displays a "500 Server Error" message for all e
### The Default 500 and 404 Templates
-By default a production application will render either a 404 or a 500 error message, in the development environment all unhandled exceptions are raised. These messages are contained in static HTML files in the `public` folder, in `404.html` and `500.html` respectively. You can customize these files to add some extra information and style, but remember that they are static HTML; i.e. you can't use ERB, SCSS, CoffeeScript, or layouts for them.
+By default a production application will render either a 404 or a 500 error message, in the development environment all unhandled exceptions are raised. These messages are contained in static HTML files in the public folder, in `404.html` and `500.html` respectively. You can customize these files to add some extra information and style, but remember that they are static HTML; i.e. you can't use ERB, SCSS, CoffeeScript, or layouts for them.
### `rescue_from`
@@ -1181,22 +1183,6 @@ NOTE: Certain exceptions are only rescuable from the `ApplicationController` cla
Force HTTPS protocol
--------------------
-Sometime you might want to force a particular controller to only be accessible via an HTTPS protocol for security reasons. You can use the `force_ssl` method in your controller to enforce that:
-
-```ruby
-class DinnerController
- force_ssl
-end
-```
-
-Just like the filter, you could also pass `:only` and `:except` to enforce the secure connection only to specific actions:
-
-```ruby
-class DinnerController
- force_ssl only: :cheeseburger
- # or
- force_ssl except: :cheeseburger
-end
-```
-
-Please note that if you find yourself adding `force_ssl` to many controllers, you may want to force the whole application to use HTTPS instead. In that case, you can set the `config.force_ssl` in your environment file.
+If you'd like to ensure that communication to your controller is only possible
+via HTTPS, you should do so by enabling the `ActionDispatch::SSL` middleware via
+`config.force_ssl` in your environment configuration.
diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md
index 96ef9c4450..662f9ea38a 100644
--- a/guides/source/action_mailer_basics.md
+++ b/guides/source/action_mailer_basics.md
@@ -20,9 +20,18 @@ Introduction
------------
Action Mailer allows you to send emails from your application using mailer classes
-and views. Mailers work very similarly to controllers. They inherit from
-`ActionMailer::Base` and live in `app/mailers`, and they have associated views
-that appear in `app/views`.
+and views.
+
+#### Mailers are similar to controllers
+
+They inherit from `ActionMailer::Base` and live in `app/mailers`. Mailers also work
+very similarly to controllers. Some examples of similarities are enumerated below.
+Mailers have:
+
+* Actions, and also, associated views that appear in `app/views`.
+* Instance variables that are accessible in views.
+* The ability to utilise layouts and partials.
+* The ability to access a params hash.
Sending Emails
--------------
@@ -60,8 +69,7 @@ end
```
As you can see, you can generate mailers just like you use other generators with
-Rails. Mailers are conceptually similar to controllers, and so we get a mailer,
-a directory for views, and a test.
+Rails.
If you didn't want to use a generator, you could create your own file inside of
`app/mailers`, just make sure that it inherits from `ActionMailer::Base`:
@@ -73,10 +81,9 @@ end
#### Edit the Mailer
-Mailers are very similar to Rails controllers. They also have methods called
-"actions" and use views to structure the content. Where a controller generates
-content like HTML to send back to the client, a Mailer creates a message to be
-delivered via email.
+Mailers have methods called "actions" and they use views to structure their content.
+Where a controller generates content like HTML to send back to the client, a Mailer
+creates a message to be delivered via email.
`app/mailers/user_mailer.rb` contains an empty mailer:
@@ -92,8 +99,8 @@ registered email address:
class UserMailer < ApplicationMailer
default from: 'notifications@example.com'
- def welcome_email(user)
- @user = user
+ def welcome_email
+ @user = params[:user]
@url = 'http://example.com/login'
mail(to: @user.email, subject: 'Welcome to My Awesome Site')
end
@@ -110,9 +117,6 @@ messages in this class. This can be overridden on a per-email basis.
* `mail` - The actual email message, we are passing the `:to` and `:subject`
headers in.
-Just like controllers, any instance variables we define in the method become
-available for use in the views.
-
#### Create a Mailer View
Create a file called `welcome_email.html.erb` in `app/views/user_mailer/`. This
@@ -176,7 +180,7 @@ $ bin/rails db:migrate
Now that we have a user model to play with, we will just edit the
`app/controllers/users_controller.rb` make it instruct the `UserMailer` to deliver
an email to the newly created user by editing the create action and inserting a
-call to `UserMailer.welcome_email` right after the user is successfully saved.
+call to `UserMailer.with(user: @user).welcome_email` right after the user is successfully saved.
Action Mailer is nicely integrated with Active Job so you can send emails outside
of the request-response cycle, so the user doesn't have to wait on it:
@@ -191,7 +195,7 @@ class UsersController < ApplicationController
respond_to do |format|
if @user.save
# Tell the UserMailer to send a welcome email after save
- UserMailer.welcome_email(@user).deliver_later
+ UserMailer.with(user: @user).welcome_email.deliver_later
format.html { redirect_to(@user, notice: 'User was successfully created.') }
format.json { render json: @user, status: :created, location: @user }
@@ -220,12 +224,17 @@ If you want to send emails right away (from a cronjob for example) just call
class SendWeeklySummary
def run
User.find_each do |user|
- UserMailer.weekly_summary(user).deliver_now
+ UserMailer.with(user: user).weekly_summary.deliver_now
end
end
end
```
+Any key value pair passed to `with` just becomes the `params` for the mailer
+action. So `with(user: @user, account: @user.account)` makes `params[:user]` and
+`params[:account]` available in the mailer action. Just like controllers have
+params.
+
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
@@ -331,7 +340,7 @@ with the addresses separated by commas.
```ruby
class AdminMailer < ApplicationMailer
- default to: Proc.new { Admin.pluck(:email) },
+ default to: -> { Admin.pluck(:email) },
from: 'notification@example.com'
def new_registration(user)
@@ -351,8 +360,8 @@ address when they receive the email. The trick to doing that is to format the
email address in the format `"Full Name" <email>`.
```ruby
-def welcome_email(user)
- @user = user
+def welcome_email
+ @user = params[:user]
email_with_name = %("#{@user.name}" <#{@user.email}>)
mail(to: email_with_name, subject: 'Welcome to My Awesome Site')
end
@@ -372,8 +381,8 @@ To change the default mailer view for your action you do something like:
class UserMailer < ApplicationMailer
default from: 'notifications@example.com'
- def welcome_email(user)
- @user = user
+ def welcome_email
+ @user = params[:user]
@url = 'http://example.com/login'
mail(to: @user.email,
subject: 'Welcome to My Awesome Site',
@@ -394,8 +403,8 @@ templates or even render inline or text without using a template file:
class UserMailer < ApplicationMailer
default from: 'notifications@example.com'
- def welcome_email(user)
- @user = user
+ def welcome_email
+ @user = params[:user]
@url = 'http://example.com/login'
mail(to: @user.email,
subject: 'Welcome to My Awesome Site') do |format|
@@ -453,8 +462,8 @@ the format block to specify different layouts for different formats:
```ruby
class UserMailer < ApplicationMailer
- def welcome_email(user)
- mail(to: user.email) do |format|
+ def welcome_email
+ mail(to: params[:user].email) do |format|
format.html { render layout: 'my_layout' }
format.text
end
@@ -477,7 +486,7 @@ special URL that renders them. In the above example, the preview class for
```ruby
class UserMailerPreview < ActionMailer::Preview
def welcome_email
- UserMailer.welcome_email(User.first)
+ UserMailer.with(user: User.first).welcome_email
end
end
```
@@ -594,12 +603,12 @@ mailer action.
```ruby
class UserMailer < ApplicationMailer
- def welcome_email(user, company)
- @user = user
+ def welcome_email
+ @user = params[:user]
@url = user_url(@user)
- delivery_options = { user_name: company.smtp_user,
- password: company.smtp_password,
- address: company.smtp_host }
+ delivery_options = { user_name: params[:company].smtp_user,
+ password: params[:company].smtp_password,
+ address: params[:company].smtp_host }
mail(to: @user.email,
subject: "Please see the Terms and Conditions attached",
delivery_method_options: delivery_options)
@@ -616,9 +625,9 @@ will default to `text/plain` otherwise.
```ruby
class UserMailer < ApplicationMailer
- def welcome_email(user, email_body)
- mail(to: user.email,
- body: email_body,
+ def welcome_email
+ mail(to: params[:user].email,
+ body: params[:email_body],
content_type: "text/html",
subject: "Already rendered!")
end
@@ -677,24 +686,43 @@ Action Mailer allows for you to specify a `before_action`, `after_action` and
* You could use a `before_action` to populate the mail object with defaults,
delivery_method_options or insert default headers and attachments.
+```ruby
+class InvitationsMailer < ApplicationMailer
+ before_action { @inviter, @invitee = params[:inviter], params[:invitee] }
+ before_action { @account = params[:inviter].account }
+
+ default to: -> { @invitee.email_address },
+ from: -> { common_address(@inviter) },
+ reply_to: -> { @inviter.email_address_with_name }
+
+ def account_invitation
+ mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
+ end
+
+ def project_invitation
+ @project = params[:project]
+ @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
+
+ mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
+ end
+end
+```
+
* You could use an `after_action` to do similar setup as a `before_action` but
using instance variables set in your mailer action.
```ruby
class UserMailer < ApplicationMailer
+ before_action { @business, @user = params[:business], params[:user] }
+
after_action :set_delivery_options,
:prevent_delivery_to_guests,
:set_business_headers
- def feedback_message(business, user)
- @business = business
- @user = user
- mail
+ def feedback_message
end
- def campaign_message(business, user)
- @business = business
- @user = user
+ def campaign_message
end
private
@@ -781,9 +809,9 @@ config.action_mailer.smtp_settings = {
user_name: '<username>',
password: '<password>',
authentication: 'plain',
- enable_starttls_auto: true }
+ enable_starttls_auto: true }
```
-Note: As of July 15, 2014, Google increased [its security measures](https://support.google.com/accounts/answer/6010255) and now blocks attempts from apps it deems less secure.
+NOTE: As of July 15, 2014, Google increased [its security measures](https://support.google.com/accounts/answer/6010255) and now blocks attempts from apps it deems less secure.
You can change your Gmail settings [here](https://www.google.com/settings/security/lesssecureapps) to allow the attempts. If your Gmail account has 2-factor authentication enabled,
then you will need to set an [app password](https://myaccount.google.com/apppasswords) and use that instead of your regular password. Alternatively, you can
use another ESP to send email by replacing 'smtp.gmail.com' above with the address of your provider.
diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md
index a57623428f..37b8843d1e 100644
--- a/guides/source/action_view_overview.md
+++ b/guides/source/action_view_overview.md
@@ -149,10 +149,10 @@ end
#### Jbuilder
[Jbuilder](https://github.com/rails/jbuilder) is a gem that's
-maintained by the Rails team and included in the default Rails Gemfile.
+maintained by the Rails team and included in the default Rails `Gemfile`.
It's similar to Builder, but is used to generate JSON, instead of XML.
-If you don't have it, you can add the following to your Gemfile:
+If you don't have it, you can add the following to your `Gemfile`:
```ruby
gem 'jbuilder'
@@ -490,7 +490,7 @@ stylesheet_link_tag "application" # => <link href="/assets/application.css" medi
#### stylesheet_path
-Computes the path to a stylesheet asset in the `app/assets/stylesheets` directory. If the source filename has no extension, `.css` will be appended. Full paths from the document root will be passed through. Used internally by stylesheet_link_tag to build the stylesheet path.
+Computes the path to a stylesheet asset in the `app/assets/stylesheets` directory. If the source filename has no extension, `.css` will be appended. Full paths from the document root will be passed through. Used internally by `stylesheet_link_tag` to build the stylesheet path.
```ruby
stylesheet_path "application" # => /assets/application.css
@@ -807,20 +807,22 @@ The core method of this helper, `form_for`, gives you the ability to create a fo
The HTML generated for this would be:
```html
-<form action="/people/create" method="post">
- <input id="person_first_name" name="person[first_name]" type="text" />
- <input id="person_last_name" name="person[last_name]" type="text" />
- <input name="commit" type="submit" value="Create" />
+<form class="new_person" id="new_person" action="/people" accept-charset="UTF-8" method="post">
+ <input name="utf8" type="hidden" value="&#x2713;" />
+ <input type="hidden" name="authenticity_token" value="lTuvBzs7ANygT0NFinXj98tfw3Emfm65wwYLbUvoWsK2pngccIQSUorM2C035M9dZswXgWTvKwFS8W5TVblpYw==" />
+ <input type="text" name="person[first_name]" id="person_first_name" />
+ <input type="text" name="person[last_name]" id="person_last_name" />
+ <input type="submit" name="commit" value="Create" data-disable-with="Create" />
</form>
```
The params object created when this form is submitted would look like:
```ruby
-{ "action" => "create", "controller" => "people", "person" => { "first_name" => "William", "last_name" => "Smith" } }
+{"utf8" => "✓", "authenticity_token" => "lTuvBzs7ANygT0NFinXj98tfw3Emfm65wwYLbUvoWsK2pngccIQSUorM2C035M9dZswXgWTvKwFS8W5TVblpYw==", "person" => {"first_name" => "William", "last_name" => "Smith"}, "commit" => "Create", "controller" => "people", "action" => "create"}
```
-The params hash has a nested person value, which can therefore be accessed with params[:person] in the controller.
+The params hash has a nested person value, which can therefore be accessed with `params[:person]` in the controller.
#### check_box
@@ -1100,7 +1102,7 @@ Possible output:
</optgroup>
```
-Note: Only the `optgroup` and `option` tags are returned, so you still have to wrap the output in an appropriate `select` tag.
+NOTE: Only the `optgroup` and `option` tags are returned, so you still have to wrap the output in an appropriate `select` tag.
#### options_for_select
@@ -1111,7 +1113,7 @@ options_for_select([ "VISA", "MasterCard" ])
# => <option>VISA</option> <option>MasterCard</option>
```
-Note: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag.
+NOTE: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag.
#### options_from_collection_for_select
@@ -1128,7 +1130,7 @@ options_from_collection_for_select(@project.people, "id", "name")
# => <option value="#{person.id}">#{person.name}</option>
```
-Note: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag.
+NOTE: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag.
#### select
@@ -1160,7 +1162,7 @@ Returns a string of option tags for pretty much any time zone in the world.
Returns select and option tags for the given object and method, using `time_zone_options_for_select` to generate the list of option tags.
```ruby
-time_zone_select( "user", "time_zone")
+time_zone_select("user", "time_zone")
```
#### date_field
@@ -1265,8 +1267,8 @@ password_field_tag 'pass'
Creates a radio button; use groups of radio buttons named the same to allow users to select from a group of options.
```ruby
-radio_button_tag 'gender', 'male'
-# => <input id="gender_male" name="gender" type="radio" value="male" />
+radio_button_tag 'favorite_color', 'maroon'
+# => <input id="favorite_color_maroon" name="favorite_color" type="radio" value="maroon" />
```
#### select_tag
diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md
index 914ef2c327..6d52ac0a99 100644
--- a/guides/source/active_job_basics.md
+++ b/guides/source/active_job_basics.md
@@ -147,7 +147,7 @@ class GuestsCleanupJob < ApplicationJob
#....
end
-# Now your job will use `resque` as it's backend queue adapter overriding what
+# Now your job will use `resque` as its backend queue adapter overriding what
# was configured in `config.active_job.queue_adapter`.
```
@@ -276,7 +276,7 @@ class GuestsCleanupJob < ApplicationJob
end
private
- def around_cleanup(job)
+ def around_cleanup
# Do something before perform
yield
# Do something after perform
@@ -290,7 +290,7 @@ For example, you could send metrics for every job enqueued:
```ruby
class ApplicationJob
- before_enqueue { |job| $statsd.increment "#{job.name.underscore}.enqueue" }
+ before_enqueue { |job| $statsd.increment "#{job.class.name.underscore}.enqueue" }
end
```
@@ -339,8 +339,23 @@ UserMailer.welcome(@user).deliver_later # Email will be localized to Esperanto.
```
-GlobalID
---------
+Supported types for arguments
+----------------------------
+
+ActiveJob supports the following types of arguments by default:
+
+ - Basic types (`NilClass`, `String`, `Integer`, `Float`, `BigDecimal`, `TrueClass`, `FalseClass`)
+ - `Symbol`
+ - `Date`
+ - `Time`
+ - `DateTime`
+ - `ActiveSupport::TimeWithZone`
+ - `ActiveSupport::Duration`
+ - `Hash` (Keys should be of `String` or `Symbol` type)
+ - `ActiveSupport::HashWithIndifferentAccess`
+ - `Array`
+
+### GlobalID
Active Job supports GlobalID for parameters. This makes it possible to pass live
Active Record objects to your job instead of class/id pairs, which you then have
@@ -368,6 +383,39 @@ end
This works with any class that mixes in `GlobalID::Identification`, which
by default has been mixed into Active Record classes.
+### Serializers
+
+You can extend the list of supported argument types. You just need to define your own serializer:
+
+```ruby
+class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
+ # Checks if an argument should be serialized by this serializer.
+ def serialize?(argument)
+ argument.is_a? Money
+ end
+
+ # Converts an object to a simpler representative using supported object types.
+ # The recommended representative is a Hash with a specific key. Keys can be of basic types only.
+ # You should call `super` to add the custom serializer type to the hash.
+ def serialize(money)
+ super(
+ "amount" => money.amount,
+ "currency" => money.currency
+ )
+ end
+
+ # Converts serialized value into a proper object.
+ def deserialize(hash)
+ Money.new(hash["amount"], hash["currency"])
+ end
+end
+```
+
+and add this serializer to the list:
+
+```ruby
+Rails.application.config.active_job.custom_serializers << MoneySerializer
+```
Exceptions
----------
diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md
index b8f076a27b..ee0472621b 100644
--- a/guides/source/active_model_basics.md
+++ b/guides/source/active_model_basics.md
@@ -464,7 +464,7 @@ a `password` accessor with certain validations on it.
#### Requirements
`ActiveModel::SecurePassword` depends on [`bcrypt`](https://github.com/codahale/bcrypt-ruby 'BCrypt'),
-so include this gem in your Gemfile to use `ActiveModel::SecurePassword` correctly.
+so include this gem in your `Gemfile` to use `ActiveModel::SecurePassword` correctly.
In order to make this work, the model must have an accessor named `password_digest`.
The `has_secure_password` will add the following validations on the `password` accessor:
diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md
index 11aefcb05f..2f85b765a3 100644
--- a/guides/source/active_record_basics.md
+++ b/guides/source/active_record_basics.md
@@ -38,13 +38,15 @@ object on how to write to and read from the database.
### Object Relational Mapping
-Object Relational Mapping, commonly referred to as its abbreviation ORM, is
+[Object Relational Mapping](https://en.wikipedia.org/wiki/Object-relational_mapping), commonly referred to as its abbreviation ORM, is
a technique that connects the rich objects of an application to tables in
a relational database management system. Using ORM, the properties and
relationships of the objects in an application can be easily stored and
retrieved from a database without writing SQL statements directly and with less
overall database access code.
+NOTE: If you are not familiar enough with relational database management systems (RDBMS) or structured query language (SQL), please go through [this tutorial](https://www.w3schools.com/sql/default.asp) (or [this one](http://www.sqlcourse.com/)) or study them by other means. Understanding how relational databases work is crucial to understanding Active Records and Rails in general.
+
### Active Record as an ORM Framework
Active Record gives us several mechanisms, the most important being the ability
@@ -142,7 +144,7 @@ end
This will create a `Product` model, mapped to a `products` table at the
database. By doing this you'll also have the ability to map the columns of each
row in that table with the attributes of the instances of your model. Suppose
-that the `products` table was created using an SQL statement like:
+that the `products` table was created using an SQL (or one of its extensions) statement like:
```sql
CREATE TABLE products (
@@ -152,8 +154,9 @@ CREATE TABLE products (
);
```
-Following the table schema above, you would be able to write code like the
-following:
+Schema above declares a table with two columns: `id` and `name`. Each row of
+this table represents a certain product with these two parameters. Thus, you
+would be able to write code like the following:
```ruby
p = Product.new
@@ -304,6 +307,17 @@ user = User.find_by(name: 'David')
user.destroy
```
+If you'd like to delete several records in bulk, you may use `destroy_all`
+method:
+
+```ruby
+# find and delete all users named David
+User.where(name: 'David').destroy_all
+
+# delete all users
+User.destroy_all
+```
+
Validations
-----------
diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md
index 53417f012e..4f54b4c206 100644
--- a/guides/source/active_record_callbacks.md
+++ b/guides/source/active_record_callbacks.md
@@ -213,6 +213,7 @@ The following methods trigger callbacks:
* `save!`
* `save(validate: false)`
* `toggle!`
+* `touch`
* `update_attribute`
* `update`
* `update!`
@@ -245,7 +246,6 @@ Just as with validations, it is also possible to skip callbacks by using the fol
* `increment`
* `increment_counter`
* `toggle`
-* `touch`
* `update_column`
* `update_columns`
* `update_all`
@@ -264,7 +264,7 @@ The whole callback chain is wrapped in a transaction. If any callback raises an
throw :abort
```
-WARNING. Any exception that is not `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception.
+WARNING. Any exception that is not `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` may break code that does not expect methods like `save` and `update` (which normally try to return `true` or `false`) to raise an exception.
Relational Callbacks
--------------------
diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md
index f8f36bf600..d09adf3c19 100644
--- a/guides/source/active_record_migrations.md
+++ b/guides/source/active_record_migrations.md
@@ -353,8 +353,7 @@ create_table :products, options: "ENGINE=BLACKHOLE" do |t|
end
```
-will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table
-(when using MySQL or MariaDB, the default is `ENGINE=InnoDB`).
+will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table.
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
@@ -443,7 +442,7 @@ change_column_default :products, :approved, from: true, to: false
This sets `:name` field on products to a `NOT NULL` column and the default
value of the `:approved` field from true to false.
-Note: You could also write the above `change_column_default` migration as
+NOTE: You could also write the above `change_column_default` migration as
`change_column_default :products, :approved, false`, but unlike the previous
example, this would make your migration irreversible.
@@ -918,35 +917,29 @@ Schema Dumping and You
### What are Schema Files for?
Migrations, mighty as they may be, are not the authoritative source for your
-database schema. That role falls to either `db/schema.rb` or an SQL file which
-Active Record generates by examining the database. They are not designed to be
-edited, they just represent the current state of the database.
+database schema. Your database remains the authoritative source. By default,
+Rails generates `db/schema.rb` which attempts to capture the current state of
+your database schema.
-There is no need (and it is error prone) to deploy a new instance of an app by
-replaying the entire migration history. It is much simpler and faster to just
-load into the database a description of the current schema.
-
-For example, this is how the test database is created: the current development
-database is dumped (either to `db/schema.rb` or `db/structure.sql`) and then
-loaded into the test database.
+It tends to be faster and less error prone to create a new instance of your
+application's database by loading the schema file via `rails db:schema:load`
+than it is to replay the entire migration history. Old migrations may fail to
+apply correctly if those migrations use changing external dependencies or rely
+on application code which evolves separately from your migrations.
Schema files are also useful if you want a quick look at what attributes an
Active Record object has. This information is not in the model's code and is
frequently spread across several migrations, but the information is nicely
-summed up in the schema file. The
-[annotate_models](https://github.com/ctran/annotate_models) gem automatically
-adds and updates comments at the top of each model summarizing the schema if
-you desire that functionality.
+summed up in the schema file.
### Types of Schema Dumps
-There are two ways to dump the schema. This is set in `config/application.rb`
-by the `config.active_record.schema_format` setting, which may be either `:sql`
-or `:ruby`.
+The format of the schema dump generated by Rails is controlled by the
+`config.active_record.schema_format` setting in `config/application.rb`. By
+default, the format is `:ruby`, but can also be set to `:sql`.
If `:ruby` is selected, then the schema is stored in `db/schema.rb`. If you look
-at this file you'll find that it looks an awful lot like one very big
-migration:
+at this file you'll find that it looks an awful lot like one very big migration:
```ruby
ActiveRecord::Schema.define(version: 20080906171750) do
@@ -968,36 +961,32 @@ end
In many ways this is exactly what it is. This file is created by inspecting the
database and expressing its structure using `create_table`, `add_index`, and so
-on. Because this is database-independent, it could be loaded into any database
-that Active Record supports. This could be very useful if you were to
-distribute an application that is able to run against multiple databases.
-
-NOTE: `db/schema.rb` cannot express database specific items such as triggers,
-sequences, stored procedures or check constraints, etc. Please note that while
-custom SQL statements can be run in migrations, these statements cannot be reconstituted
-by the schema dumper. If you are using features like 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 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
-contain. By definition, this will create a perfect copy of the database's
-structure. Using the `:sql` schema format will, however, prevent loading the
-schema into a RDBMS other than the one used to create it.
+on.
+
+`db/schema.rb` cannot express everything your database may support such as
+triggers, sequences, stored procedures, check constraints, etc. While migrations
+may use `execute` to create database constructs that are not supported by the
+Ruby migration DSL, these constructs may not be able to be reconstituted by the
+schema dumper. If you are using features like these, you should set the schema
+format to `:sql` in order to get an accurate schema file that is useful to
+create new database instances.
+
+When the schema format is set to `:sql`, the database structure will be dumped
+using a tool specific to the database into `db/structure.sql`. For example, for
+PostgreSQL, the `pg_dump` utility is used. For MySQL and MariaDB, this file will
+contain the output of `SHOW CREATE TABLE` for the various tables.
+
+To load the schema from `db/structure.sql`, run `rails db:structure:load`.
+Loading this file is done by executing the SQL statements it contains. By
+definition, this will create a perfect copy of the database's structure.
### Schema Dumps and Source Control
-Because schema dumps are the authoritative source for your database schema, it
-is strongly recommended that you check them into source control.
+Because schema files are commonly used to create new databases, it is strongly
+recommended that you check your schema file into source control.
-`db/schema.rb` contains the current version number of the database. This
-ensures conflicts are going to happen in the case of a merge where both
-branches touched the schema. When that happens, solve conflicts manually,
-keeping the highest version number of the two.
+Merge conflicts can occur in your schema file when two branches modify schema.
+To resolve these conflicts run `rails db:migrate` to regenerate the schema file.
Active Record and Referential Integrity
---------------------------------------
diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md
index 58c61f0864..6c6c6a1ded 100644
--- a/guides/source/active_record_postgresql.md
+++ b/guides/source/active_record_postgresql.md
@@ -84,7 +84,7 @@ Book.where("array_length(ratings, 1) >= 3")
### Hstore
* [type definition](https://www.postgresql.org/docs/current/static/hstore.html)
-* [functions and operators](https://www.postgresql.org/docs/current/static/hstore.html#AEN179902)
+* [functions and operators](https://www.postgresql.org/docs/current/static/hstore.html#id-1.11.7.26.5)
NOTE: You need to enable the `hstore` extension to use hstore.
@@ -290,7 +290,7 @@ SELECT n.nspname AS enum_schema,
### UUID
* [type definition](https://www.postgresql.org/docs/current/static/datatype-uuid.html)
-* [pgcrypto generator function](https://www.postgresql.org/docs/current/static/pgcrypto.html#AEN182570)
+* [pgcrypto generator function](https://www.postgresql.org/docs/current/static/pgcrypto.html#id-1.11.7.35.7)
* [uuid-ossp generator functions](https://www.postgresql.org/docs/current/static/uuid-ossp.html)
NOTE: You need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp`
diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md
index 678b80516f..4e28e31a53 100644
--- a/guides/source/active_record_querying.md
+++ b/guides/source/active_record_querying.md
@@ -414,7 +414,7 @@ end
`find_in_batches` works on model classes, as seen above, and also on relations:
```ruby
-Invoice.pending.find_in_batches do |invoice|
+Invoice.pending.find_in_batches do |invoices|
pending_invoices_export.add_invoices(invoices)
end
```
@@ -801,7 +801,7 @@ The SQL that would be executed:
SELECT * FROM articles WHERE id > 10 ORDER BY id DESC
# Original query without `only`
-SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20
+SELECT * FROM articles WHERE id > 10 ORDER BY id DESC LIMIT 20
```
@@ -820,14 +820,14 @@ Article.find(10).comments.reorder('name')
The SQL that would be executed:
```sql
-SELECT * FROM articles WHERE id = 10
+SELECT * FROM articles WHERE id = 10 LIMIT 1
SELECT * FROM comments WHERE article_id = 10 ORDER BY name
```
In the case where the `reorder` clause is not used, the SQL executed would be:
```sql
-SELECT * FROM articles WHERE id = 10
+SELECT * FROM articles WHERE id = 10 LIMIT 1
SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC
```
@@ -1091,7 +1091,7 @@ This produces:
```sql
SELECT articles.* FROM articles
- INNER JOIN categories ON articles.category_id = categories.id
+ INNER JOIN categories ON categories.id = articles.category_id
INNER JOIN comments ON comments.article_id = articles.id
```
@@ -1712,10 +1712,10 @@ Client.find_by_sql("SELECT * FROM clients
### `select_all`
-`find_by_sql` has a close relative called `connection#select_all`. `select_all` will retrieve objects from the database using custom SQL just like `find_by_sql` but will not instantiate them. Instead, you will get an array of hashes where each hash indicates a record.
+`find_by_sql` has a close relative called `connection#select_all`. `select_all` will retrieve objects from the database using custom SQL just like `find_by_sql` but will not instantiate them. This method will return an instance of `ActiveRecord::Result` class and calling `to_hash` on this object would return you an array of hashes where each hash indicates a record.
```ruby
-Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'")
+Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'").to_hash
# => [
# {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
# {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
@@ -1871,14 +1871,14 @@ All calculation methods work directly on a model:
```ruby
Client.count
-# SELECT count(*) AS count_all FROM clients
+# SELECT COUNT(*) FROM clients
```
Or on a relation:
```ruby
Client.where(first_name: 'Ryan').count
-# SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan')
+# SELECT COUNT(*) FROM clients WHERE (first_name = 'Ryan')
```
You can also use various finder methods on a relation for performing complex calculations:
@@ -1890,9 +1890,9 @@ Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received'
Which will execute:
```sql
-SELECT count(DISTINCT clients.id) AS count_all FROM clients
- LEFT OUTER JOIN orders ON orders.client_id = clients.id WHERE
- (clients.first_name = 'Ryan' AND orders.status = 'received')
+SELECT COUNT(DISTINCT clients.id) FROM clients
+ LEFT OUTER JOIN orders ON orders.client_id = clients.id
+ WHERE (clients.first_name = 'Ryan' AND orders.status = 'received')
```
### Count
diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md
index e9157f3db1..d076efcd54 100644
--- a/guides/source/active_record_validations.md
+++ b/guides/source/active_record_validations.md
@@ -953,7 +953,7 @@ should happen, an `Array` can be used. Moreover, you can apply both `:if` and
```ruby
class Computer < ApplicationRecord
validates :mouse, presence: true,
- if: [Proc.new { |c| c.market.retail? }, :desktop?],
+ if: [Proc.new { |c| c.market.retail? }, :desktop?],
unless: Proc.new { |c| c.trackpad.present? }
end
```
diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md
new file mode 100644
index 0000000000..4a915b1083
--- /dev/null
+++ b/guides/source/active_storage_overview.md
@@ -0,0 +1,597 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+
+Active Storage Overview
+=======================
+
+This guide covers how to attach files to your Active Record models.
+
+After reading this guide, you will know:
+
+* How to attach one or many files to a record.
+* How to delete an attached file.
+* How to link to an attached file.
+* How to use variants to transform images.
+* How to generate an image representation of a non-image file, such as a PDF or a video.
+* How to send file uploads directly from browsers to a storage service,
+ bypassing your application servers.
+* How to clean up files stored during testing.
+* How to implement support for additional storage services.
+
+--------------------------------------------------------------------------------
+
+What is Active Storage?
+-----------------------
+
+Active Storage facilitates uploading files to a cloud storage service like
+Amazon S3, Google Cloud Storage, or Microsoft Azure Storage and attaching those
+files to Active Record objects. It comes with a local disk-based service for
+development and testing and supports mirroring files to subordinate services for
+backups and migrations.
+
+Using Active Storage, an application can transform image uploads with
+[ImageMagick](https://www.imagemagick.org), generate image representations of
+non-image uploads like PDFs and videos, and extract metadata from arbitrary
+files.
+
+## Setup
+
+Active Storage uses two tables in your application’s database named
+`active_storage_blobs` and `active_storage_attachments`. After upgrading your
+application to Rails 5.2, run `rails active_storage:install` to generate a
+migration that creates these tables. Use `rails db:migrate` to run the
+migration.
+
+Declare Active Storage services in `config/storage.yml`. For each service your
+application uses, provide a name and the requisite configuration. The example
+below declares three services named `local`, `test`, and `amazon`:
+
+```yaml
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
+
+test:
+ service: Disk
+ root: <%= Rails.root.join("tmp/storage") %>
+
+amazon:
+ service: S3
+ access_key_id: ""
+ secret_access_key: ""
+```
+
+Tell Active Storage which service to use by setting
+`Rails.application.config.active_storage.service`. Because each environment will
+likely use a different service, it is recommended to do this on a
+per-environment basis. To use the disk service from the previous example in the
+development environment, you would add the following to
+`config/environments/development.rb`:
+
+```ruby
+# Store files locally.
+config.active_storage.service = :local
+```
+
+To use the Amazon S3 service in production, you add the following to
+`config/environments/production.rb`:
+
+```ruby
+# Store files on Amazon S3.
+config.active_storage.service = :amazon
+```
+
+Continue reading for more information on the built-in service adapters (e.g.
+`Disk` and `S3`) and the configuration they require.
+
+### Disk Service
+
+Declare a Disk service in `config/storage.yml`:
+
+```yaml
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
+```
+
+### Amazon S3 Service
+
+Declare an S3 service in `config/storage.yml`:
+
+```yaml
+amazon:
+ service: S3
+ access_key_id: ""
+ secret_access_key: ""
+ region: ""
+ bucket: ""
+```
+
+Add the [`aws-sdk-s3`](https://github.com/aws/aws-sdk-ruby) gem to your `Gemfile`:
+
+```ruby
+gem "aws-sdk-s3", require: false
+```
+
+NOTE: The core features of Active Storage require the following permissions: `s3:ListBucket`, `s3:PutObject`, `s3:GetObject`, and `s3:DeleteObject`. If you have additional upload options configured such as setting ACLs then additional permissions may be required.
+
+NOTE: If you want to use environment variables, standard SDK configuration files, profiles,
+IAM instance profiles or task roles, you can omit the `access_key_id`, `secret_access_key`,
+and `region` keys in the example above. The Amazon S3 Service supports all of the
+authentication options described in the [AWS SDK documentation]
+(https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/setup-config.html).
+
+
+### Microsoft Azure Storage Service
+
+Declare an Azure Storage service in `config/storage.yml`:
+
+```yaml
+azure:
+ service: AzureStorage
+ storage_account_name: ""
+ storage_access_key: ""
+ container: ""
+```
+
+Add the [`azure-storage`](https://github.com/Azure/azure-storage-ruby) gem to your `Gemfile`:
+
+```ruby
+gem "azure-storage", require: false
+```
+
+### Google Cloud Storage Service
+
+Declare a Google Cloud Storage service in `config/storage.yml`:
+
+```yaml
+google:
+ service: GCS
+ credentials: <%= Rails.root.join("path/to/keyfile.json") %>
+ project: ""
+ bucket: ""
+```
+
+Optionally provide a Hash of credentials instead of a keyfile path:
+
+```yaml
+google:
+ service: GCS
+ credentials:
+ type: "service_account"
+ project_id: ""
+ private_key_id: <%= Rails.application.credentials.dig(:gcs, :private_key_id) %>
+ private_key: <%= Rails.application.credentials.dig(:gcs, :private_key) %>
+ client_email: ""
+ client_id: ""
+ auth_uri: "https://accounts.google.com/o/oauth2/auth"
+ token_uri: "https://accounts.google.com/o/oauth2/token"
+ auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
+ client_x509_cert_url: ""
+ project: ""
+ bucket: ""
+```
+
+Add the [`google-cloud-storage`](https://github.com/GoogleCloudPlatform/google-cloud-ruby/tree/master/google-cloud-storage) gem to your `Gemfile`:
+
+```ruby
+gem "google-cloud-storage", "~> 1.8", require: false
+```
+
+### Mirror Service
+
+You can keep multiple services in sync by defining a mirror service. When a file
+is uploaded or deleted, it's done across all the mirrored services. Mirrored
+services can be used to facilitate a migration between services in production.
+You can start mirroring to the new service, copy existing files from the old
+service to the new, then go all-in on the new service. Define each of the
+services you'd like to use as described above and reference them from a mirrored
+service.
+
+```yaml
+s3_west_coast:
+ service: S3
+ access_key_id: ""
+ secret_access_key: ""
+ region: ""
+ bucket: ""
+
+s3_east_coast:
+ service: S3
+ access_key_id: ""
+ secret_access_key: ""
+ region: ""
+ bucket: ""
+
+production:
+ service: Mirror
+ primary: s3_east_coast
+ mirrors:
+ - s3_west_coast
+```
+
+NOTE: Files are served from the primary service.
+
+Attaching Files to Records
+--------------------------
+
+### `has_one_attached`
+
+The `has_one_attached` macro sets up a one-to-one mapping between records and
+files. Each record can have one file attached to it.
+
+For example, suppose your application has a `User` model. If you want each user to
+have an avatar, define the `User` model like this:
+
+```ruby
+class User < ApplicationRecord
+ has_one_attached :avatar
+end
+```
+
+You can create a user with an avatar:
+
+```ruby
+class SignupController < ApplicationController
+ def create
+ user = User.create!(user_params)
+ session[:user_id] = user.id
+ redirect_to root_path
+ end
+
+ private
+ def user_params
+ params.require(:user).permit(:email_address, :password, :avatar)
+ end
+end
+```
+
+Call `avatar.attach` to attach an avatar to an existing user:
+
+```ruby
+Current.user.avatar.attach(params[:avatar])
+```
+
+Call `avatar.attached?` to determine whether a particular user has an avatar:
+
+```ruby
+Current.user.avatar.attached?
+```
+
+### `has_many_attached`
+
+The `has_many_attached` macro sets up a one-to-many relationship between records
+and files. Each record can have many files attached to it.
+
+For example, suppose your application has a `Message` model. If you want each
+message to have many images, define the `Message` model like this:
+
+```ruby
+class Message < ApplicationRecord
+ has_many_attached :images
+end
+```
+
+You can create a message with images:
+
+```ruby
+class MessagesController < ApplicationController
+ def create
+ message = Message.create!(message_params)
+ redirect_to message
+ end
+
+ private
+ def message_params
+ params.require(:message).permit(:title, :content, images: [])
+ end
+end
+```
+
+Call `images.attach` to add new images to an existing message:
+
+```ruby
+@message.images.attach(params[:images])
+```
+
+Call `images.attached?` to determine whether a particular message has any images:
+
+```ruby
+@message.images.attached?
+```
+
+Removing Files
+--------------
+
+To remove an attachment from a model, call `purge` on the attachment. Removal
+can be done in the background if your application is setup to use Active Job.
+Purging deletes the blob and the file from the storage service.
+
+```ruby
+# Synchronously destroy the avatar and actual resource files.
+user.avatar.purge
+
+# Destroy the associated models and actual resource files async, via Active Job.
+user.avatar.purge_later
+```
+
+Linking to Files
+----------------
+
+Generate a permanent URL for the blob that points to the application. Upon
+access, a redirect to the actual service endpoint is returned. This indirection
+decouples the public URL from the actual one, and allows, for example, mirroring
+attachments in different services for high-availability. The redirection has an
+HTTP expiration of 5 min.
+
+```ruby
+url_for(user.avatar)
+```
+
+To create a download link, use the `rails_blob_{path|url}` helper. Using this
+helper allows you to set the disposition.
+
+```ruby
+rails_blob_path(user.avatar, disposition: "attachment")
+```
+
+Transforming Images
+-------------------
+
+To create variation of the image, call `variant` on the Blob. You can pass
+any transformation to the method supported by the processor. The default
+processor is [MiniMagick](https://github.com/minimagick/minimagick), but you
+can also use [Vips](http://www.rubydoc.info/gems/ruby-vips/Vips/Image).
+
+To enable variants, add the `image_processing` gem to your `Gemfile`:
+
+```ruby
+gem 'image_processing', '~> 1.2'
+```
+
+When the browser hits the variant URL, Active Storage will lazy transform the
+original blob into the format you specified and redirect to its new service
+location.
+
+```erb
+<%= image_tag user.avatar.variant(resize_to_fit: [100, 100]) %>
+```
+
+To switch to the Vips processor, you would add the following to
+`config/application.rb`:
+
+```ruby
+# Use Vips for processing variants.
+config.active_storage.variant_processor = :vips
+```
+
+Previewing Files
+----------------
+
+Some non-image files can be previewed: that is, they can be presented as images.
+For example, a video file can be previewed by extracting its first frame. Out of
+the box, Active Storage supports previewing videos and PDF documents.
+
+```erb
+<ul>
+ <% @message.files.each do |file| %>
+ <li>
+ <%= image_tag file.preview(resize_to_limit: [100, 100]) %>
+ </li>
+ <% end %>
+</ul>
+```
+
+WARNING: Extracting previews requires third-party applications, `ffmpeg` for
+video and `mutool` for PDFs. These libraries are not provided by Rails. You must
+install them yourself to use the built-in previewers. Before you install and use
+third-party software, make sure you understand the licensing implications of
+doing so.
+
+Direct Uploads
+--------------
+
+Active Storage, with its included JavaScript library, supports uploading
+directly from the client to the cloud.
+
+### Direct upload installation
+
+1. Include `activestorage.js` in your application's JavaScript bundle.
+
+ Using the asset pipeline:
+
+ ```js
+ //= require activestorage
+
+ ```
+
+ Using the npm package:
+
+ ```js
+ import * as ActiveStorage from "activestorage"
+ ActiveStorage.start()
+ ```
+
+2. Annotate file inputs with the direct upload URL.
+
+ ```ruby
+ <%= form.file_field :attachments, multiple: true, direct_upload: true %>
+ ```
+3. That's it! Uploads begin upon form submission.
+
+### Direct upload JavaScript events
+
+| Event name | Event target | Event data (`event.detail`) | Description |
+| --- | --- | --- | --- |
+| `direct-uploads:start` | `<form>` | None | A form containing files for direct upload fields was submitted. |
+| `direct-upload:initialize` | `<input>` | `{id, file}` | Dispatched for every file after form submission. |
+| `direct-upload:start` | `<input>` | `{id, file}` | A direct upload is starting. |
+| `direct-upload:before-blob-request` | `<input>` | `{id, file, xhr}` | Before making a request to your application for direct upload metadata. |
+| `direct-upload:before-storage-request` | `<input>` | `{id, file, xhr}` | Before making a request to store a file. |
+| `direct-upload:progress` | `<input>` | `{id, file, progress}` | As requests to store files progress. |
+| `direct-upload:error` | `<input>` | `{id, file, error}` | An error occurred. An `alert` will display unless this event is canceled. |
+| `direct-upload:end` | `<input>` | `{id, file}` | A direct upload has ended. |
+| `direct-uploads:end` | `<form>` | None | All direct uploads have ended. |
+
+### Example
+
+You can use these events to show the progress of an upload.
+
+![direct-uploads](https://user-images.githubusercontent.com/5355/28694528-16e69d0c-72f8-11e7-91a7-c0b8cfc90391.gif)
+
+To show the uploaded files in a form:
+
+```js
+// direct_uploads.js
+
+addEventListener("direct-upload:initialize", event => {
+ const { target, detail } = event
+ const { id, file } = detail
+ target.insertAdjacentHTML("beforebegin", `
+ <div id="direct-upload-${id}" class="direct-upload direct-upload--pending">
+ <div id="direct-upload-progress-${id}" class="direct-upload__progress" style="width: 0%"></div>
+ <span class="direct-upload__filename">${file.name}</span>
+ </div>
+ `)
+})
+
+addEventListener("direct-upload:start", event => {
+ const { id } = event.detail
+ const element = document.getElementById(`direct-upload-${id}`)
+ element.classList.remove("direct-upload--pending")
+})
+
+addEventListener("direct-upload:progress", event => {
+ const { id, progress } = event.detail
+ const progressElement = document.getElementById(`direct-upload-progress-${id}`)
+ progressElement.style.width = `${progress}%`
+})
+
+addEventListener("direct-upload:error", event => {
+ event.preventDefault()
+ const { id, error } = event.detail
+ const element = document.getElementById(`direct-upload-${id}`)
+ element.classList.add("direct-upload--error")
+ element.setAttribute("title", error)
+})
+
+addEventListener("direct-upload:end", event => {
+ const { id } = event.detail
+ const element = document.getElementById(`direct-upload-${id}`)
+ element.classList.add("direct-upload--complete")
+})
+```
+
+Add styles:
+
+```css
+/* direct_uploads.css */
+
+.direct-upload {
+ display: inline-block;
+ position: relative;
+ padding: 2px 4px;
+ margin: 0 3px 3px 0;
+ border: 1px solid rgba(0, 0, 0, 0.3);
+ border-radius: 3px;
+ font-size: 11px;
+ line-height: 13px;
+}
+
+.direct-upload--pending {
+ opacity: 0.6;
+}
+
+.direct-upload__progress {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ opacity: 0.2;
+ background: #0076ff;
+ transition: width 120ms ease-out, opacity 60ms 60ms ease-in;
+ transform: translate3d(0, 0, 0);
+}
+
+.direct-upload--complete .direct-upload__progress {
+ opacity: 0.4;
+}
+
+.direct-upload--error {
+ border-color: red;
+}
+
+input[type=file][data-direct-upload-url][disabled] {
+ display: none;
+}
+```
+
+Discarding Files Stored During System Tests
+-------------------------------------------
+
+System tests clean up test data by rolling back a transaction. Because destroy
+is never called on an object, the attached files are never cleaned up. If you
+want to clear the files, you can do it in an `after_teardown` callback. Doing it
+here ensures that all connections created during the test are complete and
+you won't receive an error from Active Storage saying it can't find a file.
+
+```ruby
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
+
+ def remove_uploaded_files
+ FileUtils.rm_rf("#{Rails.root}/storage_test")
+ end
+
+ def after_teardown
+ super
+ remove_uploaded_files
+ end
+end
+```
+
+If your system tests verify the deletion of a model with attachments and you're
+using Active Job, set your test environment to use the inline queue adapter so
+the purge job is executed immediately rather at an unknown time in the future.
+
+You may also want to use a separate service definition for the test environment
+so your tests don't delete the files you create during development.
+
+```ruby
+# Use inline job processing to make things happen immediately
+config.active_job.queue_adapter = :inline
+
+# Separate file storage in the test environment
+config.active_storage.service = :local_test
+```
+
+Discarding Files Stored During Integration Tests
+-------------------------------------------
+
+Similarly to System Tests, files uploaded during Integration Tests will not be
+automatically cleaned up. If you want to clear the files, you can do it in an
+`after_teardown` callback. Doing it here ensures that all connections created
+during the test are complete and you won't receive an error from Active Storage
+saying it can't find a file.
+
+```ruby
+module ActionDispatch
+ class IntegrationTest
+ def remove_uploaded_files
+ FileUtils.rm_rf(Rails.root.join('tmp', 'storage'))
+ end
+
+ def after_teardown
+ super
+ remove_uploaded_files
+ end
+ end
+end
+```
+
+Implementing Support for Other Cloud Services
+---------------------------------------------
+
+If you need to support a cloud service other than these, you will need to
+implement the Service. Each service extends
+[`ActiveStorage::Service`](https://github.com/rails/rails/blob/master/activestorage/lib/active_storage/service.rb)
+by implementing the methods necessary to upload and download files to the cloud.
diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md
index 1438245f9c..ae2e1faf14 100644
--- a/guides/source/active_support_core_extensions.md
+++ b/guides/source/active_support_core_extensions.md
@@ -135,16 +135,14 @@ NOTE: Defined in `active_support/core_ext/object/blank.rb`.
### `duplicable?`
-In Ruby 2.4 most objects can be duplicated via `dup` or `clone` except
-methods and certain numbers. Though Ruby 2.2 and 2.3 can't duplicate `nil`,
-`false`, `true`, and symbols as well as instances `Float`, `Fixnum`,
-and `Bignum` instances.
+As of Ruby 2.5, most objects can be duplicated via `dup` or `clone`:
```ruby
"foo".dup # => "foo"
"".dup # => ""
-1.method(:+).dup # => TypeError: allocator undefined for Method
-Complex(0).dup # => TypeError: can't copy Complex
+Rational(1).dup # => (1/1)
+Complex(0).dup # => (0+0i)
+1.method(:+).dup # => TypeError (allocator undefined for Method)
```
Active Support provides `duplicable?` to query an object about this:
@@ -152,35 +150,18 @@ Active Support provides `duplicable?` to query an object about this:
```ruby
"foo".duplicable? # => true
"".duplicable? # => true
-Rational(1).duplicable? # => false
-Complex(1).duplicable? # => false
+Rational(1).duplicable? # => true
+Complex(1).duplicable? # => true
1.method(:+).duplicable? # => false
```
-`duplicable?` matches Ruby's `dup` according to the Ruby version.
-
-So in 2.4:
-
-```ruby
-nil.dup # => nil
-:my_symbol.dup # => :my_symbol
-1.dup # => 1
-
-nil.duplicable? # => true
-:my_symbol.duplicable? # => true
-1.duplicable? # => true
-```
-
-Whereas in 2.2 and 2.3:
+`duplicable?` matches the current Ruby version's `dup` behavior,
+so results will vary according the version of Ruby you're using.
+In Ruby 2.4, for example, Complex and Rational are not duplicable:
```ruby
-nil.dup # => TypeError: can't dup NilClass
-:my_symbol.dup # => TypeError: can't dup Symbol
-1.dup # => TypeError: can't dup Fixnum
-
-nil.duplicable? # => false
-:my_symbol.duplicable? # => false
-1.duplicable? # => false
+Rational(1).duplicable? # => false
+Complex(1).duplicable? # => false
```
WARNING: Any class can disallow duplication by removing `dup` and `clone` or raising exceptions from them. Thus only `rescue` can tell whether a given arbitrary object is duplicable. `duplicable?` depends on the hard-coded list above, but it is much faster than `rescue`. Use it only if you know the hard-coded list is enough in your use case.
@@ -674,44 +655,6 @@ M.parents # => [X::Y, X, Object]
NOTE: Defined in `active_support/core_ext/module/introspection.rb`.
-### Reachable
-
-A named module is reachable if it is stored in its corresponding constant. It means you can reach the module object via the constant.
-
-That is what ordinarily happens, if a module is called "M", the `M` constant exists and holds it:
-
-```ruby
-module M
-end
-
-M.reachable? # => true
-```
-
-But since constants and modules are indeed kind of decoupled, module objects can become unreachable:
-
-```ruby
-module M
-end
-
-orphan = Object.send(:remove_const, :M)
-
-# The module object is orphan now but it still has a name.
-orphan.name # => "M"
-
-# You cannot reach it via the constant M because it does not even exist.
-orphan.reachable? # => false
-
-# Let's define a module called "M" again.
-module M
-end
-
-# The constant M exists now again, and it stores a module
-# object called "M", but it is a new instance.
-orphan.reachable? # => false
-```
-
-NOTE: Defined in `active_support/core_ext/module/reachable.rb`.
-
### Anonymous
A module may or may not have a name:
@@ -745,7 +688,6 @@ end
m = Object.send(:remove_const, :M)
-m.reachable? # => false
m.anonymous? # => false
```
@@ -837,6 +779,14 @@ delegate :size, to: :attachment, prefix: :avatar
In the previous example the macro generates `avatar_size` rather than `size`.
+The option `:private` changes methods scope:
+
+```ruby
+delegate :date_of_birth, to: :profile, private: true
+```
+
+The delegated methods are public by default. Pass `private: true` to change that.
+
NOTE: Defined in `active_support/core_ext/module/delegation.rb`
#### `delegate_missing_to`
@@ -964,6 +914,15 @@ The macros `cattr_reader`, `cattr_writer`, and `cattr_accessor` are analogous to
```ruby
class MysqlAdapter < AbstractAdapter
# Generates class methods to access @@emulate_booleans.
+ cattr_accessor :emulate_booleans
+end
+```
+
+Also, you can pass a block to `cattr_*` to set up the attribute with a default value:
+
+```ruby
+class MysqlAdapter < AbstractAdapter
+ # Generates class methods to access @@emulate_booleans with default value of true.
cattr_accessor :emulate_booleans, default: true
end
```
@@ -980,15 +939,6 @@ end
we can access `field_error_proc` in views.
-Also, you can pass a block to `cattr_*` to set up the attribute with a default value:
-
-```ruby
-class MysqlAdapter < AbstractAdapter
- # Generates class methods to access @@emulate_booleans with default value of true.
- cattr_accessor :emulate_booleans, default: true
-end
-```
-
The generation of the reader instance method can be prevented by setting `:instance_reader` to `false` and the generation of the writer instance method can be prevented by setting `:instance_writer` to `false`. Generation of both methods can be prevented by setting `:instance_accessor` to `false`. In all cases, the value must be exactly `false` and not any false value.
```ruby
@@ -1791,7 +1741,7 @@ The methods `to_date`, `to_time`, and `to_datetime` are basically convenience wr
"2010-07-27 23:42:00".to_time(:local) # => 2010-07-27 23:42:00 +0200
```
-Default is `:utc`.
+Default is `:local`.
Please refer to the documentation of `Date._parse` for further details.
@@ -1835,7 +1785,7 @@ NOTE: Defined in `active_support/core_ext/numeric/bytes.rb`.
### Time
-Enables the use of time calculations and declarations, like `45.minutes + 2.hours + 4.years`.
+Enables the use of time calculations and declarations, like `45.minutes + 2.hours + 4.weeks`.
These methods use Time#advance for precise date calculations when using from_now, ago, etc.
as well as adding or subtracting their results from a Time object. For example:
@@ -1844,13 +1794,15 @@ as well as adding or subtracting their results from a Time object. For example:
# equivalent to Time.current.advance(months: 1)
1.month.from_now
-# equivalent to Time.current.advance(years: 2)
-2.years.from_now
+# equivalent to Time.current.advance(weeks: 2)
+2.weeks.from_now
-# equivalent to Time.current.advance(months: 4, years: 5)
-(4.months + 5.years).from_now
+# equivalent to Time.current.advance(months: 4, weeks: 5)
+(4.months + 5.weeks).from_now
```
+WARNING. For other durations please refer to the time extensions to `Integer`.
+
NOTE: Defined in `active_support/core_ext/numeric/time.rb`.
### Formatting
@@ -1986,6 +1938,28 @@ The method `ordinalize` returns the ordinal string corresponding to the receiver
NOTE: Defined in `active_support/core_ext/integer/inflections.rb`.
+### Time
+
+Enables the use of time calculations and declarations, like `4.months + 5.years`.
+
+These methods use Time#advance for precise date calculations when using from_now, ago, etc.
+as well as adding or subtracting their results from a Time object. For example:
+
+```ruby
+# equivalent to Time.current.advance(months: 1)
+1.month.from_now
+
+# equivalent to Time.current.advance(years: 2)
+2.years.from_now
+
+# equivalent to Time.current.advance(months: 4, years: 5)
+(4.months + 5.years).from_now
+```
+
+WARNING. For other durations please refer to the time extensions to `Numeric`.
+
+NOTE: Defined in `active_support/core_ext/integer/time.rb`.
+
Extensions to `BigDecimal`
--------------------------
### `to_s`
@@ -1993,19 +1967,19 @@ Extensions to `BigDecimal`
The method `to_s` provides a default specifier of "F". This means that a simple call to `to_s` will result in floating point representation instead of engineering notation:
```ruby
-BigDecimal.new(5.00, 6).to_s # => "5.0"
+BigDecimal(5.00, 6).to_s # => "5.0"
```
and that symbol specifiers are also supported:
```ruby
-BigDecimal.new(5.00, 6).to_s(:db) # => "5.0"
+BigDecimal(5.00, 6).to_s(:db) # => "5.0"
```
Engineering notation is still supported:
```ruby
-BigDecimal.new(5.00, 6).to_s("e") # => "0.5E1"
+BigDecimal(5.00, 6).to_s("e") # => "0.5E1"
```
Extensions to `Enumerable`
@@ -2791,20 +2765,6 @@ Active Record does not accept unknown options when building associations, for ex
NOTE: Defined in `active_support/core_ext/hash/keys.rb`.
-### Working with Values
-
-#### `transform_values` && `transform_values!`
-
-The method `transform_values` accepts a block and returns a hash that has applied the block operations to each of the values in the receiver.
-
-```ruby
-{ nil => nil, 1 => 1, :x => :a }.transform_values { |value| value.to_s.upcase }
-# => {nil=>"", 1=>"1", :x=>"A"}
-```
-There's also the bang variant `transform_values!` that applies the block operations to values in the very receiver.
-
-NOTE: Defined in `active_support/core_ext/hash/transform_values.rb`.
-
### Slicing
Ruby has built-in support for taking slices out of strings and arrays. Active Support extends slicing to hashes:
@@ -2866,16 +2826,6 @@ The method `with_indifferent_access` returns an `ActiveSupport::HashWithIndiffer
NOTE: Defined in `active_support/core_ext/hash/indifferent_access.rb`.
-### Compacting
-
-The methods `compact` and `compact!` return a Hash without items with `nil` value.
-
-```ruby
-{a: 1, b: 2, c: nil}.compact # => {a: 1, b: 2}
-```
-
-NOTE: Defined in `active_support/core_ext/hash/compact.rb`.
-
Extensions to `Regexp`
----------------------
@@ -2905,24 +2855,6 @@ end
NOTE: Defined in `active_support/core_ext/regexp.rb`.
-### `match?`
-
-Rails implements `Regexp#match?` for Ruby versions prior to 2.4:
-
-```ruby
-/oo/.match?('foo') # => true
-/oo/.match?('bar') # => false
-/oo/.match?('foo', 1) # => true
-```
-
-The backport has the same interface and lack of side-effects in the caller like
-not setting `$1` and friends, but it does not have the speed benefits. Its
-purpose is to be able to write 2.4 compatible code. Rails itself uses this
-predicate internally for example.
-
-Active Support defines `Regexp#match?` only if not present, so code running
-under 2.4 or later does run the original one and gets the performance boost.
-
Extensions to `Range`
---------------------
@@ -2985,6 +2917,32 @@ Extensions to `Date`
NOTE: All the following methods are defined in `active_support/core_ext/date/calculations.rb`.
+```ruby
+yesterday
+tomorrow
+beginning_of_week (at_beginning_of_week)
+end_of_week (at_end_of_week)
+monday
+sunday
+weeks_ago
+prev_week (last_week)
+next_week
+months_ago
+months_since
+beginning_of_month (at_beginning_of_month)
+end_of_month (at_end_of_month)
+last_month
+beginning_of_quarter (at_beginning_of_quarter)
+end_of_quarter (at_end_of_quarter)
+beginning_of_year (at_beginning_of_year)
+end_of_year (at_end_of_year)
+years_ago
+years_since
+last_year
+on_weekday?
+on_weekend?
+```
+
INFO: The following calculation methods have edge cases in October 1582, since days 5..14 just do not exist. This guide does not document their behavior around those days for brevity, but it is enough to say that they do what you would expect. That is, `Date.new(1582, 10, 4).tomorrow` returns `Date.new(1582, 10, 15)` and so on. Please check `test/core_ext/date_ext_test.rb` in the Active Support test suite for expected behavior.
#### `Date.current`
@@ -2995,68 +2953,6 @@ When making Date comparisons using methods which honor the user time zone, make
#### Named dates
-##### `prev_year`, `next_year`
-
-In Ruby 1.9 `prev_year` and `next_year` return a date with the same day/month in the last or next year:
-
-```ruby
-d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
-d.prev_year # => Fri, 08 May 2009
-d.next_year # => Sun, 08 May 2011
-```
-
-If date is the 29th of February of a leap year, you obtain the 28th:
-
-```ruby
-d = Date.new(2000, 2, 29) # => Tue, 29 Feb 2000
-d.prev_year # => Sun, 28 Feb 1999
-d.next_year # => Wed, 28 Feb 2001
-```
-
-`prev_year` is aliased to `last_year`.
-
-##### `prev_month`, `next_month`
-
-In Ruby 1.9 `prev_month` and `next_month` return the date with the same day in the last or next month:
-
-```ruby
-d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
-d.prev_month # => Thu, 08 Apr 2010
-d.next_month # => Tue, 08 Jun 2010
-```
-
-If such a day does not exist, the last day of the corresponding month is returned:
-
-```ruby
-Date.new(2000, 5, 31).prev_month # => Sun, 30 Apr 2000
-Date.new(2000, 3, 31).prev_month # => Tue, 29 Feb 2000
-Date.new(2000, 5, 31).next_month # => Fri, 30 Jun 2000
-Date.new(2000, 1, 31).next_month # => Tue, 29 Feb 2000
-```
-
-`prev_month` is aliased to `last_month`.
-
-##### `prev_quarter`, `next_quarter`
-
-Same as `prev_month` and `next_month`. It returns the date with the same day in the previous or next quarter:
-
-```ruby
-t = Time.local(2010, 5, 8) # => Sat, 08 May 2010
-t.prev_quarter # => Mon, 08 Feb 2010
-t.next_quarter # => Sun, 08 Aug 2010
-```
-
-If such a day does not exist, the last day of the corresponding month is returned:
-
-```ruby
-Time.local(2000, 7, 31).prev_quarter # => Sun, 30 Apr 2000
-Time.local(2000, 5, 31).prev_quarter # => Tue, 29 Feb 2000
-Time.local(2000, 10, 31).prev_quarter # => Mon, 30 Oct 2000
-Time.local(2000, 11, 31).next_quarter # => Wed, 28 Feb 2001
-```
-
-`prev_quarter` is aliased to `last_quarter`.
-
##### `beginning_of_week`, `end_of_week`
The methods `beginning_of_week` and `end_of_week` return the dates for the
@@ -3174,6 +3070,8 @@ Date.new(2012, 2, 29).years_ago(3) # => Sat, 28 Feb 2009
Date.new(2012, 2, 29).years_since(3) # => Sat, 28 Feb 2015
```
+`last_year` is short-hand for `#years_ago(1)`.
+
##### `months_ago`, `months_since`
The methods `months_ago` and `months_since` work analogously for months:
@@ -3190,6 +3088,8 @@ Date.new(2010, 4, 30).months_ago(2) # => Sun, 28 Feb 2010
Date.new(2009, 12, 31).months_since(2) # => Sun, 28 Feb 2010
```
+`last_month` is short-hand for `#months_ago(1)`.
+
##### `weeks_ago`
The method `weeks_ago` works analogously for weeks:
@@ -3352,35 +3252,7 @@ WARNING: `DateTime` is not aware of DST rules and so some of these methods have
NOTE: All the following methods are defined in `active_support/core_ext/date_time/calculations.rb`.
-The class `DateTime` is a subclass of `Date` so by loading `active_support/core_ext/date/calculations.rb` you inherit these methods and their aliases, except that they will always return datetimes:
-
-```ruby
-yesterday
-tomorrow
-beginning_of_week (at_beginning_of_week)
-end_of_week (at_end_of_week)
-monday
-sunday
-weeks_ago
-prev_week (last_week)
-next_week
-months_ago
-months_since
-beginning_of_month (at_beginning_of_month)
-end_of_month (at_end_of_month)
-prev_month (last_month)
-next_month
-beginning_of_quarter (at_beginning_of_quarter)
-end_of_quarter (at_end_of_quarter)
-beginning_of_year (at_beginning_of_year)
-end_of_year (at_end_of_year)
-years_ago
-years_since
-prev_year (last_year)
-next_year
-on_weekday?
-on_weekend?
-```
+The class `DateTime` is a subclass of `Date` so by loading `active_support/core_ext/date/calculations.rb` you inherit these methods and their aliases, except that they will always return datetimes.
The following methods are reimplemented so you do **not** need to load `active_support/core_ext/date/calculations.rb` for these ones:
@@ -3528,8 +3400,6 @@ Extensions to `Time`
NOTE: All the following methods are defined in `active_support/core_ext/time/calculations.rb`.
-Active Support adds to `Time` many of the methods available for `DateTime`:
-
```ruby
past?
today?
@@ -3541,6 +3411,8 @@ change
advance
ago
since (in)
+prev_day
+next_day
beginning_of_day (midnight, at_midnight, at_beginning_of_day)
end_of_day
beginning_of_hour (at_beginning_of_hour)
@@ -3556,15 +3428,17 @@ months_ago
months_since
beginning_of_month (at_beginning_of_month)
end_of_month (at_end_of_month)
-prev_month (last_month)
+prev_month
next_month
+last_month
beginning_of_quarter (at_beginning_of_quarter)
end_of_quarter (at_end_of_quarter)
beginning_of_year (at_beginning_of_year)
end_of_year (at_end_of_year)
years_ago
years_since
-prev_year (last_year)
+prev_year
+last_year
next_year
on_weekday?
on_weekend?
@@ -3622,6 +3496,74 @@ now.all_year
# => Fri, 01 Jan 2010 00:00:00 UTC +00:00..Fri, 31 Dec 2010 23:59:59 UTC +00:00
```
+#### `prev_day`, `next_day`
+
+In Ruby 1.9 `prev_day` and `next_day` return the date in the last or next day:
+
+```ruby
+d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
+d.prev_day # => Fri, 07 May 2010
+d.next_day # => Sun, 09 May 2010
+```
+
+#### `prev_month`, `next_month`
+
+In Ruby 1.9 `prev_month` and `next_month` return the date with the same day in the last or next month:
+
+```ruby
+d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
+d.prev_month # => Thu, 08 Apr 2010
+d.next_month # => Tue, 08 Jun 2010
+```
+
+If such a day does not exist, the last day of the corresponding month is returned:
+
+```ruby
+Date.new(2000, 5, 31).prev_month # => Sun, 30 Apr 2000
+Date.new(2000, 3, 31).prev_month # => Tue, 29 Feb 2000
+Date.new(2000, 5, 31).next_month # => Fri, 30 Jun 2000
+Date.new(2000, 1, 31).next_month # => Tue, 29 Feb 2000
+```
+
+#### `prev_year`, `next_year`
+
+In Ruby 1.9 `prev_year` and `next_year` return a date with the same day/month in the last or next year:
+
+```ruby
+d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
+d.prev_year # => Fri, 08 May 2009
+d.next_year # => Sun, 08 May 2011
+```
+
+If date is the 29th of February of a leap year, you obtain the 28th:
+
+```ruby
+d = Date.new(2000, 2, 29) # => Tue, 29 Feb 2000
+d.prev_year # => Sun, 28 Feb 1999
+d.next_year # => Wed, 28 Feb 2001
+```
+
+#### `prev_quarter`, `next_quarter`
+
+`prev_quarter` and `next_quarter` return the date with the same day in the previous or next quarter:
+
+```ruby
+t = Time.local(2010, 5, 8) # => 2010-05-08 00:00:00 +0300
+t.prev_quarter # => 2010-02-08 00:00:00 +0200
+t.next_quarter # => 2010-08-08 00:00:00 +0300
+```
+
+If such a day does not exist, the last day of the corresponding month is returned:
+
+```ruby
+Time.local(2000, 7, 31).prev_quarter # => 2000-04-30 00:00:00 +0300
+Time.local(2000, 5, 31).prev_quarter # => 2000-02-29 00:00:00 +0200
+Time.local(2000, 10, 31).prev_quarter # => 2000-07-31 00:00:00 +0300
+Time.local(2000, 11, 31).next_quarter # => 2001-03-01 00:00:00 +0200
+```
+
+`prev_quarter` is aliased to `last_quarter`.
+
### Time Constructors
Active Support defines `Time.current` to be `Time.zone.now` if there's a user time zone defined, with fallback to `Time.now`:
diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md
index 03c9183eb3..11c4a8222a 100644
--- a/guides/source/active_support_instrumentation.md
+++ b/guides/source/active_support_instrumentation.md
@@ -197,6 +197,12 @@ INFO. Additional keys may be added by the caller.
}
```
+### unpermitted_parameters.action_controller
+
+| Key | Value |
+| ------- | ---------------- |
+| `:keys` | Unpermitted keys |
+
Action View
-----------
@@ -304,7 +310,7 @@ Action Mailer
mailer: "Notification",
message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
subject: "Rails Guides",
- to: ["users@rails.com", "ddh@rails.com"],
+ to: ["users@rails.com", "dhh@rails.com"],
from: ["me@rails.com"],
date: Sat, 10 Mar 2012 14:18:09 +0100,
mail: "..." # omitted for brevity
@@ -330,13 +336,29 @@ Action Mailer
mailer: "Notification",
message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
subject: "Rails Guides",
- to: ["users@rails.com", "ddh@rails.com"],
+ to: ["users@rails.com", "dhh@rails.com"],
from: ["me@rails.com"],
date: Sat, 10 Mar 2012 14:18:09 +0100,
mail: "..." # omitted for brevity
}
```
+### process.action_mailer
+
+| Key | Value |
+| ------------- | ------------------------ |
+| `:mailer` | Name of the mailer class |
+| `:action` | The action |
+| `:args` | The arguments |
+
+```ruby
+{
+ mailer: "Notification",
+ action: "welcome_email",
+ args: []
+}
+```
+
Active Support
--------------
@@ -450,6 +472,99 @@ Active Job
| `:adapter` | QueueAdapter object processing the job |
| `:job` | Job object |
+Action Cable
+------------
+
+### perform_action.action_cable
+
+| Key | Value |
+| ---------------- | ------------------------- |
+| `:channel_class` | Name of the channel class |
+| `:action` | The action |
+| `:data` | A hash of data |
+
+### transmit.action_cable
+
+| Key | Value |
+| ---------------- | ------------------------- |
+| `:channel_class` | Name of the channel class |
+| `:data` | A hash of data |
+| `:via` | Via |
+
+### transmit_subscription_confirmation.action_cable
+
+| Key | Value |
+| ---------------- | ------------------------- |
+| `:channel_class` | Name of the channel class |
+
+### transmit_subscription_rejection.action_cable
+
+| Key | Value |
+| ---------------- | ------------------------- |
+| `:channel_class` | Name of the channel class |
+
+### broadcast.action_cable
+
+| Key | Value |
+| --------------- | -------------------- |
+| `:broadcasting` | A named broadcasting |
+| `:message` | A hash of message |
+| `:coder` | The coder |
+
+Active Storage
+--------------
+
+### service_upload.active_storage
+
+| Key | Value |
+| ------------ | ---------------------------- |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+| `:checksum` | Checksum to ensure integrity |
+
+### service_streaming_download.active_storage
+
+| Key | Value |
+| ------------ | ------------------- |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+
+### service_download.active_storage
+
+| Key | Value |
+| ------------ | ------------------- |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+
+### service_delete.active_storage
+
+| Key | Value |
+| ------------ | ------------------- |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+
+### service_delete_prefixed.active_storage
+
+| Key | Value |
+| ------------ | ------------------- |
+| `:prefix` | Key prefix |
+| `:service` | Name of the service |
+
+### service_exist.active_storage
+
+| Key | Value |
+| ------------ | --------------------------- |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+| `:exist` | File or blob exists or not |
+
+### service_url.active_storage
+
+| Key | Value |
+| ------------ | ------------------- |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+| `:url` | Generated url |
Railties
--------
@@ -549,4 +664,4 @@ end
```
You should follow Rails conventions when defining your own events. The format is: `event.library`.
-If you application is sending Tweets, you should create an event named `tweet.twitter`.
+If your application is sending Tweets, you should create an event named `tweet.twitter`.
diff --git a/guides/source/api_app.md b/guides/source/api_app.md
index da1b7b25ef..c2df6c45ad 100644
--- a/guides/source/api_app.md
+++ b/guides/source/api_app.md
@@ -1,6 +1,5 @@
**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
-
Using Rails for API-only Applications
=====================================
@@ -25,7 +24,7 @@ With the advent of client-side frameworks, more developers are using Rails to
build a back-end that is shared between their web application and other native
applications.
-For example, Twitter uses its [public API](https://dev.twitter.com) in its web
+For example, Twitter uses its [public API](https://developer.twitter.com/) in its web
application, which is built as a static site that consumes JSON resources.
Instead of using Rails to generate HTML that communicates with the server
@@ -216,7 +215,6 @@ An API application comes with the following middleware by default:
- `Rack::Head`
- `Rack::ConditionalGet`
- `Rack::ETag`
-- `MyApi::Application::Routes`
See the [internal middleware](rails_on_rack.html#internal-middleware-stack)
section of the Rack guide for further information on them.
@@ -377,7 +375,6 @@ controller modules by default:
- `ActionController::ConditionalGet`: Support for `stale?`.
- `ActionController::BasicImplicitRender`: Makes sure to return an empty response, if there isn't an explicit one.
- `ActionController::StrongParameters`: Support for parameters white-listing in combination with Active Model mass assignment.
-- `ActionController::ForceSSL`: Support for `force_ssl`.
- `ActionController::DataStreaming`: Support for `send_file` and `send_data`.
- `AbstractController::Callbacks`: Support for `before_action` and
similar helpers.
@@ -415,8 +412,10 @@ Some common modules you might want to add:
- `AbstractController::Translation`: Support for the `l` and `t` localization
and translation methods.
-- `ActionController::HttpAuthentication::Basic` (or `Digest` or `Token`): Support
- for basic, digest or token HTTP authentication.
+- Support for basic, digest or token HTTP authentication:
+ * `ActionController::HttpAuthentication::Basic::ControllerMethods`,
+ * `ActionController::HttpAuthentication::Digest::ControllerMethods`,
+ * `ActionController::HttpAuthentication::Token::ControllerMethods`
- `ActionView::Layouts`: Support for layouts when rendering.
- `ActionController::MimeResponds`: Support for `respond_to`.
- `ActionController::Cookies`: Support for `cookies`, which includes
diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md
index 2c153d3783..10b89433e7 100644
--- a/guides/source/api_documentation_guidelines.md
+++ b/guides/source/api_documentation_guidelines.md
@@ -16,7 +16,7 @@ RDoc
----
The [Rails API documentation](http://api.rubyonrails.org) is generated with
-[RDoc](http://docs.seattlerb.org/rdoc/). To generate it, make sure you are
+[RDoc](https://ruby.github.io/rdoc/). To generate it, make sure you are
in the rails root directory, run `bundle install` and execute:
```bash
@@ -26,9 +26,9 @@ in the rails root directory, run `bundle install` and execute:
Resulting HTML files can be found in the ./doc/rdoc directory.
Please consult the RDoc documentation for help with the
-[markup](http://docs.seattlerb.org/rdoc/RDoc/Markup.html),
+[markup](https://ruby.github.io/rdoc/RDoc/Markup.html),
and also take into account these [additional
-directives](http://docs.seattlerb.org/rdoc/RDoc/Parser/Ruby.html).
+directives](https://ruby.github.io/rdoc/RDoc/Parser/Ruby.html).
Wording
-------
diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md
index 17ab9c7600..88b87b78d2 100644
--- a/guides/source/asset_pipeline.md
+++ b/guides/source/asset_pipeline.md
@@ -22,8 +22,7 @@ The asset pipeline provides a framework to concatenate and minify or compress
JavaScript and CSS assets. It also adds the ability to write these assets in
other languages and pre-processors such as CoffeeScript, Sass and ERB.
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.
+from other gems.
The asset pipeline is implemented by the
[sprockets-rails](https://github.com/rails/sprockets-rails) gem,
@@ -35,7 +34,7 @@ rails new appname --skip-sprockets
```
Rails automatically adds the `sass-rails`, `coffee-rails` and `uglifier`
-gems to your Gemfile, which are used by Sprockets for asset compression:
+gems to your `Gemfile`, which are used by Sprockets for asset compression:
```ruby
gem 'sass-rails'
@@ -44,8 +43,8 @@ gem 'coffee-rails'
```
Using the `--skip-sprockets` option will prevent Rails from adding
-them to your Gemfile, so if you later want to enable
-the asset pipeline you will have to add those gems to your Gemfile. Also,
+them to your `Gemfile`, so if you later want to enable
+the asset pipeline you will have to add those gems to your `Gemfile`. Also,
creating an application with the `--skip-sprockets` option will generate
a slightly different `config/application.rb` file, with a require statement
for the sprockets railtie that is commented-out. You will have to remove
@@ -65,7 +64,7 @@ config.assets.js_compressor = :uglifier
```
NOTE: The `sass-rails` gem is automatically used for CSS compression if included
-in the Gemfile and no `config.assets.css_compressor` option is set.
+in the `Gemfile` and no `config.assets.css_compressor` option is set.
### Main Features
@@ -154,7 +153,7 @@ environments. You can enable or disable it in your configuration through the
More reading:
-* [Optimize caching](http://code.google.com/speed/page-speed/docs/caching.html)
+* [Optimize caching](https://developers.google.com/speed/docs/insights/LeverageBrowserCaching)
* [Revving Filenames: don't use querystring](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/)
@@ -181,7 +180,7 @@ When you generate a scaffold or a controller, Rails also generates a JavaScript
file (or CoffeeScript file if the `coffee-rails` gem is in the `Gemfile`) and a
Cascading Style Sheet file (or SCSS file if `sass-rails` is in the `Gemfile`)
for that controller. Additionally, when generating a scaffold, Rails generates
-the file scaffolds.css (or scaffolds.scss if `sass-rails` is in the
+the file `scaffolds.css` (or `scaffolds.scss` if `sass-rails` is in the
`Gemfile`.)
For example, if you generate a `ProjectsController`, Rails will also add a new
@@ -202,7 +201,7 @@ will result in your assets being included more than once.
WARNING: When using asset precompilation, you will need to ensure that your
controller assets will be precompiled when loading them on a per page basis. By
-default .coffee and .scss files will not be precompiled on their own. See
+default `.coffee` and `.scss` files will not be precompiled on their own. See
[Precompiling Assets](#precompiling-assets) for more information on how
precompiling works.
@@ -726,11 +725,11 @@ include, you can add them to the `precompile` array in `config/initializers/asse
Rails.application.config.assets.precompile += %w( admin.js admin.css )
```
-NOTE. Always specify an expected compiled filename that ends with .js or .css,
+NOTE. Always specify an expected compiled filename that ends with `.js` or `.css`,
even if you want to add Sass or CoffeeScript files to the precompile array.
-The task also generates a `.sprockets-manifest-md5hash.json` (where `md5hash` is
-an MD5 hash) that contains a list with all your assets and their respective
+The task also generates a `.sprockets-manifest-randomhex.json` (where `randomhex` is
+a 16-byte random hex string) that contains a list with all your assets and their respective
fingerprints. This is used by the Rails helper methods to avoid handing the
mapping requests back to Sprockets. A typical manifest file looks like:
@@ -850,7 +849,7 @@ This mode uses more memory, performs more poorly than the default and is not
recommended.
If you are deploying a production application to a system without any
-pre-existing JavaScript runtimes, you may want to add one to your Gemfile:
+pre-existing JavaScript runtimes, you may want to add one to your `Gemfile`:
```ruby
group :production do
@@ -918,7 +917,7 @@ config.action_controller.asset_host = ENV['CDN_HOST']
-Note: You would need to set `CDN_HOST` on your server to `mycdnsubdomain
+NOTE: You would need to set `CDN_HOST` on your server to `mycdnsubdomain
.fictional-cdn.com` for this to work.
Once you have configured your server and your CDN when you serve a webpage that
@@ -1090,7 +1089,7 @@ Possible options for JavaScript compression are `:closure`, `:uglifier` and
`:yui`. These require the use of the `closure-compiler`, `uglifier` or
`yui-compressor` gems, respectively.
-The default Gemfile includes [uglifier](https://github.com/lautis/uglifier).
+The default `Gemfile` includes [uglifier](https://github.com/lautis/uglifier).
This gem wraps [UglifyJS](https://github.com/mishoo/UglifyJS) (written for
NodeJS) in Ruby. It compresses your code by removing white space and comments,
shortening local variable names, and performing other micro-optimizations such
@@ -1205,10 +1204,10 @@ Adding Assets to Your Gems
Assets can also come from external sources in the form of gems.
-A good example of this is the `jquery-rails` gem which comes with Rails as the
-standard JavaScript library gem. This gem contains an engine class which
-inherits from `Rails::Engine`. By doing this, Rails is informed that the
-directory for this gem may contain assets and the `app/assets`, `lib/assets` and
+A good example of this is the `jquery-rails` gem.
+This gem contains an engine class which inherits from `Rails::Engine`.
+By doing this, Rails is informed that the directory for this
+gem may contain assets and the `app/assets`, `lib/assets` and
`vendor/assets` directories of this engine are added to the search path of
Sprockets.
@@ -1219,7 +1218,7 @@ Sprockets uses Processors, Transformers, Compressors, and Exporters to extend
Sprockets functionality. Have a look at
[Extending Sprockets](https://github.com/rails/sprockets/blob/master/guides/extending_sprockets.md)
to learn more. Here we registered a preprocessor to add a comment to the end
-of text/css (.css) files.
+of text/css (`.css`) files.
```ruby
module AddComment
@@ -1244,11 +1243,7 @@ moving the files from `public/` to the new locations. See [Asset
Organization](#asset-organization) above for guidance on the correct locations
for different file types.
-Next will be avoiding duplicate JavaScript files. Since jQuery is the default
-JavaScript library from Rails 3.1 onwards, you don't need to copy `jquery.js`
-into `app/assets` and it will be included automatically.
-
-The third is updating the various environment files with the correct default
+Next is updating the various environment files with the correct default
options.
In `application.rb`:
diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md
index b5bd24d027..6fbd52edbd 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -94,7 +94,7 @@ class Book < ApplicationRecord
end
```
-![belongs_to Association Diagram](images/belongs_to.png)
+![belongs_to Association Diagram](images/association_basics/belongs_to.png)
NOTE: `belongs_to` associations _must_ use the singular term. If you used the pluralized form in the above example for the `author` association in the `Book` model, you would be told that there was an "uninitialized constant Book::Authors". This is because Rails automatically infers the class name from the association name. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too.
@@ -127,7 +127,7 @@ class Supplier < ApplicationRecord
end
```
-![has_one Association Diagram](images/has_one.png)
+![has_one Association Diagram](images/association_basics/has_one.png)
The corresponding migration might look like this:
@@ -171,7 +171,7 @@ end
NOTE: The name of the other model is pluralized when declaring a `has_many` association.
-![has_many Association Diagram](images/has_many.png)
+![has_many Association Diagram](images/association_basics/has_many.png)
The corresponding migration might look like this:
@@ -213,7 +213,7 @@ class Patient < ApplicationRecord
end
```
-![has_many :through Association Diagram](images/has_many_through.png)
+![has_many :through Association Diagram](images/association_basics/has_many_through.png)
The corresponding migration might look like this:
@@ -299,7 +299,7 @@ class AccountHistory < ApplicationRecord
end
```
-![has_one :through Association Diagram](images/has_one_through.png)
+![has_one :through Association Diagram](images/association_basics/has_one_through.png)
The corresponding migration might look like this:
@@ -340,7 +340,7 @@ class Part < ApplicationRecord
end
```
-![has_and_belongs_to_many Association Diagram](images/habtm.png)
+![has_and_belongs_to_many Association Diagram](images/association_basics/habtm.png)
The corresponding migration might look like this:
@@ -494,7 +494,7 @@ class CreatePictures < ActiveRecord::Migration[5.0]
end
```
-![Polymorphic Association Diagram](images/polymorphic.png)
+![Polymorphic Association Diagram](images/association_basics/polymorphic.png)
### Self Joins
@@ -505,7 +505,7 @@ class Employee < ApplicationRecord
has_many :subordinates, class_name: "Employee",
foreign_key: "manager_id"
- belongs_to :manager, class_name: "Employee"
+ belongs_to :manager, class_name: "Employee", optional: true
end
```
@@ -572,40 +572,32 @@ class Book < ApplicationRecord
end
```
-This declaration needs to be backed up by the proper foreign key declaration on the books table:
+This declaration needs to be backed up by a corresponding foreign key column in the books table. For a brand new table, the migration might look something like this:
```ruby
class CreateBooks < ActiveRecord::Migration[5.0]
def change
create_table :books do |t|
- t.datetime :published_at
- t.string :book_number
- t.integer :author_id
+ t.datetime :published_at
+ t.string :book_number
+ t.references :author
end
end
end
```
-If you create an association some time after you build the underlying model, you need to remember to create an `add_column` migration to provide the necessary foreign key.
-
-It's a good practice to add an index on the foreign key to improve queries
-performance and a foreign key constraint to ensure referential data integrity:
+Whereas for an existing table, it might look like this:
```ruby
-class CreateBooks < ActiveRecord::Migration[5.0]
+class AddAuthorToBooks < ActiveRecord::Migration[5.0]
def change
- create_table :books do |t|
- t.datetime :published_at
- t.string :book_number
- t.integer :author_id
- end
-
- add_index :books, :author_id
- add_foreign_key :books, :authors
+ add_reference :books, :author
end
end
```
+NOTE: If you wish to [enforce referential integrity at the database level](/active_record_migrations.html#foreign-keys), add the `foreign_key: true` option to the ‘reference’ column declarations above.
+
#### Creating Join Tables for `has_and_belongs_to_many` Associations
If you create a `has_and_belongs_to_many` association, you need to explicitly create the joining table. Unless the name of the join table is explicitly specified by using the `:join_table` option, Active Record creates the name by using the lexical book of the class names. So a join between author and book models will give the default join table name of "authors_books" because "a" outranks "b" in lexical ordering.
@@ -735,12 +727,9 @@ a.first_name = 'David'
a.first_name == b.author.first_name # => true
```
-Active Record supports automatic identification for most associations with standard names. However, Active Record will not automatically identify bi-directional associations that contain any of the following options:
+Active Record supports automatic identification for most associations with standard names. However, Active Record will not automatically identify bi-directional associations that contain a scope or any of the following options:
-* `:conditions`
* `:through`
-* `:polymorphic`
-* `:class_name`
* `:foreign_key`
For example, consider the following model declarations:
@@ -787,12 +776,6 @@ a.first_name = 'David'
a.first_name == b.writer.first_name # => true
```
-There are a few limitations to `:inverse_of` support:
-
-* They do not work with `:through` associations.
-* They do not work with `:polymorphic` associations.
-* They do not work with `:as` associations.
-
Detailed Association Reference
------------------------------
@@ -804,7 +787,7 @@ The `belongs_to` association creates a one-to-one match with another model. In d
#### Methods Added by `belongs_to`
-When you declare a `belongs_to` association, the declaring class automatically gains five methods related to the association:
+When you declare a `belongs_to` association, the declaring class automatically gains 6 methods related to the association:
* `association`
* `association=(associate)`
@@ -906,7 +889,7 @@ The `belongs_to` association supports these options:
##### `:autosave`
-If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object.
+If you set the `:autosave` option to `true`, Rails will save any loaded association members and destroy members that are marked for destruction whenever you save the parent object. Setting `:autosave` to `false` is not the same as not setting the `:autosave` option. If the `:autosave` option is not present, then new associated objects will be saved, but updated associated objects will not be saved.
##### `:class_name`
@@ -1012,7 +995,7 @@ When we execute `@user.todos.create` then the `@todo` record will have its
##### `:inverse_of`
-The `:inverse_of` option specifies the name of the `has_many` or `has_one` association that is the inverse of this association. Does not work in combination with the `:polymorphic` options.
+The `:inverse_of` option specifies the name of the `has_many` or `has_one` association that is the inverse of this association.
```ruby
class Author < ApplicationRecord
@@ -1082,7 +1065,7 @@ You can use any of the standard [querying methods](active_record_querying.html)
The `where` method lets you specify the conditions that the associated object must meet.
```ruby
-class book < ApplicationRecord
+class Book < ApplicationRecord
belongs_to :author, -> { where active: true }
end
```
@@ -1155,7 +1138,7 @@ The `has_one` association creates a one-to-one match with another model. In data
#### Methods Added by `has_one`
-When you declare a `has_one` association, the declaring class automatically gains five methods related to the association:
+When you declare a `has_one` association, the declaring class automatically gains 6 methods related to the association:
* `association`
* `association=(associate)`
@@ -1257,7 +1240,7 @@ Setting the `:as` option indicates that this is a polymorphic association. Polym
##### `:autosave`
-If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object.
+If you set the `:autosave` option to `true`, Rails will save any loaded association members and destroy members that are marked for destruction whenever you save the parent object. Setting `:autosave` to `false` is not the same as not setting the `:autosave` option. If the `:autosave` option is not present, then new associated objects will be saved, but updated associated objects will not be saved.
##### `:class_name`
@@ -1299,7 +1282,7 @@ TIP: In any case, Rails will not create foreign key columns for you. You need to
##### `:inverse_of`
-The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. Does not work in combination with the `:through` or `:as` options.
+The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association.
```ruby
class Supplier < ApplicationRecord
@@ -1428,7 +1411,7 @@ The `has_many` association creates a one-to-many relationship with another model
#### Methods Added by `has_many`
-When you declare a `has_many` association, the declaring class automatically gains 16 methods related to the association:
+When you declare a `has_many` association, the declaring class automatically gains 17 methods related to the association:
* `collection`
* `collection<<(object, ...)`
@@ -1561,7 +1544,8 @@ The `collection.size` method returns the number of objects in the collection.
##### `collection.find(...)`
-The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`.
+The `collection.find` method finds objects within the collection. It uses the same syntax and options as
+[`ActiveRecord::Base.find`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find).
```ruby
@available_book = @author.books.find(1)
@@ -1653,7 +1637,7 @@ Setting the `:as` option indicates that this is a polymorphic association, as di
##### `:autosave`
-If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object.
+If you set the `:autosave` option to `true`, Rails will save any loaded association members and destroy members that are marked for destruction whenever you save the parent object. Setting `:autosave` to `false` is not the same as not setting the `:autosave` option. If the `:autosave` option is not present, then new associated objects will be saved, but updated associated objects will not be saved.
##### `:class_name`
@@ -1693,7 +1677,7 @@ TIP: In any case, Rails will not create foreign key columns for you. You need to
##### `:inverse_of`
-The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. Does not work in combination with the `:through` or `:as` options.
+The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association.
```ruby
class Author < ApplicationRecord
@@ -1960,7 +1944,7 @@ The `has_and_belongs_to_many` association creates a many-to-many relationship wi
#### Methods Added by `has_and_belongs_to_many`
-When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 16 methods related to the association:
+When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 17 methods related to the association:
* `collection`
* `collection<<(object, ...)`
@@ -2091,7 +2075,8 @@ The `collection.size` method returns the number of objects in the collection.
##### `collection.find(...)`
-The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`. It also adds the additional condition that the object must be in the collection.
+The `collection.find` method finds objects within the collection. It uses the same syntax and options as
+[`ActiveRecord::Base.find`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find).
```ruby
@assembly = @part.assemblies.find(1)
@@ -2099,7 +2084,7 @@ The `collection.find` method finds objects within the collection. It uses the sa
##### `collection.where(...)`
-The `collection.where` method finds objects within the collection based on the conditions supplied but the objects are loaded lazily meaning that the database is queried only when the object(s) are accessed. It also adds the additional condition that the object must be in the collection.
+The `collection.where` method finds objects within the collection based on the conditions supplied but the objects are loaded lazily meaning that the database is queried only when the object(s) are accessed.
```ruby
@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)
@@ -2176,7 +2161,7 @@ end
##### `:autosave`
-If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object.
+If you set the `:autosave` option to `true`, Rails will save any loaded association members and destroy members that are marked for destruction whenever you save the parent object. Setting `:autosave` to `false` is not the same as not setting the `:autosave` option. If the `:autosave` option is not present, then new associated objects will be saved, but updated associated objects will not be saved.
##### `:class_name`
diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md
index ede0324a51..5428b16edc 100644
--- a/guides/source/autoloading_and_reloading_constants.md
+++ b/guides/source/autoloading_and_reloading_constants.md
@@ -8,7 +8,7 @@ This guide documents how constant autoloading and reloading works.
After reading this guide, you will know:
* Key aspects of Ruby constants
-* What is `autoload_paths`
+* What are the `autoload_paths` and how does eager loading work in production?
* How constant autoloading works
* What is `require_dependency`
* How constant reloading works
@@ -330,11 +330,17 @@ its resolution next. Let's define *parent* to be that qualifying class or module
object, that is, `Billing` in the example above. The algorithm for qualified
constants goes like this:
-1. The constant is looked up in the parent and its ancestors.
+1. The constant is looked up in the parent and its ancestors. In Ruby >= 2.5,
+`Object` is skipped if present among the ancestors. `Kernel` and `BasicObject`
+are still checked though.
2. If the lookup fails, `const_missing` is invoked in the parent. The default
implementation of `const_missing` raises `NameError`, but it can be overridden.
+INFO. In Ruby < 2.5 `String::Hash` evaluates to `Hash` and the interpreter
+issues a warning: "toplevel constant Hash referenced by String::Hash". Starting
+with 2.5, `String::Hash` raises `NameError` because `Object` is skipped.
+
As you see, this algorithm is simpler than the one for relative constants. In
particular, the nesting plays no role here, and modules are not special-cased,
if neither they nor their ancestors have the constants, `Object` is **not**
@@ -424,8 +430,8 @@ if `House` is still unknown when `app/models/beach_house.rb` is being eager
loaded, Rails autoloads it.
-autoload_paths
---------------
+autoload_paths and eager_load_paths
+-----------------------------------
As you probably know, when `require` gets a relative file name:
@@ -445,7 +451,7 @@ the idea is that when a constant like `Post` is hit and missing, if there's a
`post.rb` file for example in `app/models` Rails is going to find it, evaluate
it, and have `Post` defined as a side-effect.
-Alright, Rails has a collection of directories similar to `$LOAD_PATH` in which
+All right, Rails has a collection of directories similar to `$LOAD_PATH` in which
to look up `post.rb`. That collection is called `autoload_paths` and by
default it contains:
@@ -459,17 +465,22 @@ default it contains:
* The directory `test/mailers/previews`.
-Also, this collection is configurable via `config.autoload_paths`. For example,
-`lib` was in the list years ago, but no longer is. An application can opt-in
-by adding this to `config/application.rb`:
+`eager_load_paths` is initially the `app` paths above
-```ruby
-config.autoload_paths << "#{Rails.root}/lib"
-```
+How files are autoloaded depends on `eager_load` and `cache_classes` config settings which typically vary in development, production, and test modes:
+
+ * In **development**, you want quicker startup with incremental loading of application code. So `eager_load` should be set to `false`, and Rails will autoload files as needed (see [Autoloading Algorithms](#autoloading-algorithms) below) -- and then reload them when they change (see [Constant Reloading](#constant-reloading) below).
+ * In **production**, however you want consistency and thread-safety and can live with a longer boot time. So `eager_load` is set to `true`, and then during boot (before the app is ready to receive requests) Rails loads all files in the `eager_load_paths` and then turns off auto loading (NB: autoloading may be needed during eager loading). Not autoloading after boot is a `good thing`, as autoloading can cause the app to be have thread-safety problems.
+ * In **test**, for speed of execution (of individual tests) `eager_load` is `false`, so Rails follows development behaviour.
+
+What is described above are the defaults with a newly generated Rails app. There are multiple ways this can be configured differently (see [Configuring Rails Applications](configuring.html#rails-general-configuration).
+). But using `autoload_paths` on its own in the past (before Rails 5) developers might configure `autoload_paths` to add in extra locations (e.g. `lib` which used to be an autoload path list years ago, but no longer is). However this is now discouraged for most purposes, as it is likely to lead to production-only errors. It is possible to add new locations to both `config.eager_load_paths` and `config.autoload_paths` but use at your own risk.
+
+See also [Autoloading in the Test Environment](#autoloading-in-the-test-environment).
`config.autoload_paths` is not changeable from environment-specific configuration files.
-The value of `autoload_paths` can be inspected. In a just generated application
+The value of `autoload_paths` can be inspected. In a just-generated application
it is (edited):
```
@@ -1178,6 +1189,8 @@ end
#### Qualified References
+WARNING. This gotcha is only possible in Ruby < 2.5.
+
Given
```ruby
@@ -1321,3 +1334,17 @@ class C < BasicObject
end
end
```
+
+### Autoloading in the Test Environment
+
+When configuring the `test` environment for autoloading you might consider multiple factors.
+
+For example it might be worth running your tests with an identical setup to production (`config.eager_load = true`, `config.cache_classes = true`) in order to catch any problems before they hit production (this is compensation for the lack of dev-prod parity). However this will slow down the boot time for individual tests on a dev machine (and is not immediately compatible with spring see below). So one possibility is to do this on a
+[CI](https://en.wikipedia.org/wiki/Continuous_integration) machine only (which should run without spring).
+
+On a development machine you can then have your tests running with whatever is fastest (ideally `config.eager_load = false`).
+
+With the [Spring](https://github.com/rails/spring) pre-loader (included with new Rails apps), you ideally keep `config.eager_load = false` as per development. Sometimes you may end up with a hybrid configuration (`config.eager_load = true`, `config.cache_classes = true` AND `config.enable_dependency_loading = true`), see [spring issue](https://github.com/rails/spring/issues/519#issuecomment-348324369). However it might be simpler to keep the same configuration as development, and work out whatever it is that is causing autoloading to fail (perhaps by the results of your CI test results).
+
+Occasionally you may need to explicitly eager_load by using `Rails
+.application.eager_load!` in the setup of your tests -- this might occur if your [tests involve multithreading](https://stackoverflow.com/questions/25796409/in-rails-how-can-i-eager-load-all-code-before-a-specific-rspec-test).
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index 96650b5be9..3f357b532b 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -32,7 +32,7 @@ Basic Caching
This is an introduction to three types of caching techniques: page, action and
fragment caching. By default Rails provides fragment caching. In order to use
page and action caching you will need to add `actionpack-page_caching` and
-`actionpack-action_caching` to your Gemfile.
+`actionpack-action_caching` to your `Gemfile`.
By default, caching is only enabled in your production environment. To play
around with caching locally you'll want to enable caching in your local
@@ -100,9 +100,9 @@ called key-based expiration.
Cache fragments will also be expired when the view fragment changes (e.g., the
HTML in the view changes). The string of characters at the end of the key is a
-template tree digest. It is an MD5 hash computed based on the contents of the
-view fragment you are caching. If you change the view fragment, the MD5 hash
-will change, expiring the existing file.
+template tree digest. It is a hash digest computed based on the contents of the
+view fragment you are caching. If you change the view fragment, the digest will
+change, expiring the existing file.
TIP: Cache stores like Memcached will automatically delete old cache files.
@@ -366,9 +366,9 @@ There are some common options used by all cache implementations. These can be pa
* `:namespace` - This option can be used to create a namespace within the cache store. It is especially useful if your application shares a cache with other applications.
-* `:compress` - This option can be used to indicate that compression should be used in the cache. This can be useful for transferring large cache entries over a slow network.
+* `:compress` - Enabled by default. Compresses cache entries so more data can be stored in the same memory footprint, leading to fewer cache evictions and higher hit rates.
-* `:compress_threshold` - This option is used in conjunction with the `:compress` option to indicate a threshold under which cache entries should not be compressed. This defaults to 16 kilobytes.
+* `:compress_threshold` - Defaults to 1kB. Cache entries larger than this threshold, specified in bytes, are compressed.
* `:expires_in` - This option sets an expiration time in seconds for the cache entry when it will be automatically removed from the cache.
@@ -444,6 +444,78 @@ The `write` and `fetch` methods on this cache accept two additional options that
config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
```
+### ActiveSupport::Cache::RedisCacheStore
+
+The Redis cache store takes advantage of Redis support for automatic eviction
+when it reaches max memory, allowing it to behave much like a Memcached cache server.
+
+Deployment note: Redis doesn't expire keys by default, so take care to use a
+dedicated Redis cache server. Don't fill up your persistent-Redis server with
+volatile cache data! Read the
+[Redis cache server setup guide](https://redis.io/topics/lru-cache) in detail.
+
+For a cache-only Redis server, set `maxmemory-policy` to one of the variants of allkeys.
+Redis 4+ supports least-frequently-used eviction (`allkeys-lfu`), an excellent
+default choice. Redis 3 and earlier should use least-recently-used eviction (`allkeys-lru`).
+
+Set cache read and write timeouts relatively low. Regenerating a cached value
+is often faster than waiting more than a second to retrieve it. Both read and
+write timeouts default to 1 second, but may be set lower if your network is
+consistently low-latency.
+
+By default, the cache store will not attempt to reconnect to Redis if the
+connection fails during a request. If you experience frequent disconnects you
+may wish to enable reconnect attempts.
+
+Cache reads and writes never raise exceptions; they just return `nil` instead,
+behaving as if there was nothing in the cache. To gauge whether your cache is
+hitting exceptions, you may provide an `error_handler` to report to an
+exception gathering service. It must accept three keyword arguments: `method`,
+the cache store method that was originally called; `returning`, the value that
+was returned to the user, typically `nil`; and `exception`, the exception that
+was rescued.
+
+To get started, add the redis gem to your Gemfile:
+
+```ruby
+gem 'redis'
+```
+
+You can enable support for the faster [hiredis](https://github.com/redis/hiredis)
+connection library by additionally adding its ruby wrapper to your Gemfile:
+
+```ruby
+gem 'hiredis'
+```
+
+Redis cache store will automatically require & use hiredis if available. No further
+configuration is needed.
+
+Finally, add the configuration in the relevant `config/environments/*.rb` file:
+
+```ruby
+config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
+```
+
+A more complex, production Redis cache store may look something like this:
+
+```ruby
+cache_servers = %w(redis://cache-01:6379/0 redis://cache-02:6379/0)
+config.cache_store = :redis_cache_store, { url: cache_servers,
+
+ connect_timeout: 30, # Defaults to 20 seconds
+ read_timeout: 0.2, # Defaults to 1 second
+ write_timeout: 0.2, # Defaults to 1 second
+ reconnect_attempts: 1, # Defaults to 0
+
+ error_handler: -> (method:, returning:, exception:) {
+ # Report errors to Sentry as warnings
+ Raven.capture_exception exception, level: 'warning',
+ tags: { method: method, returning: returning }
+ }
+}
+```
+
### ActiveSupport::Cache::NullStore
This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with `Rails.cache` but caching may interfere with being able to see the results of code changes. With this cache store, all `fetch` and `read` operations will result in a miss.
diff --git a/guides/source/command_line.md b/guides/source/command_line.md
index 2cd8e02a77..b41e8bbec6 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -290,7 +290,7 @@ INFO: You can also use the alias "c" to invoke the console: `rails c`.
You can specify the environment in which the `console` command should operate.
```bash
-$ bin/rails console staging
+$ bin/rails console -e staging
```
If you wish to test out some code without changing any data, you can do that by invoking `rails console --sandbox`.
@@ -431,16 +431,16 @@ INFO: You can also use `bin/rails -T` to get the list of tasks.
```bash
$ bin/rails about
About your application's environment
-Rails version 5.1.0
-Ruby version 2.2.2 (x86_64-linux)
-RubyGems version 2.4.6
-Rack version 2.0.1
+Rails version 6.0.0
+Ruby version 2.5.0 (x86_64-linux)
+RubyGems version 2.7.3
+Rack version 2.0.4
JavaScript Runtime Node.js (V8)
Middleware: Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, ActiveSupport::Cache::Strategy::LocalCache::Middleware, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, ActionDispatch::RemoteIp, Sprockets::Rails::QuietAssets, Rails::Rack::Logger, ActionDispatch::ShowExceptions, WebConsole::Middleware, ActionDispatch::DebugExceptions, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, Rack::Head, Rack::ConditionalGet, Rack::ETag
Application root /home/foobar/commandsapp
Environment development
Database adapter sqlite3
-Database schema version 20110805173523
+Database schema version 20180205173523
```
### `assets`
@@ -659,6 +659,6 @@ development:
...
```
-It also generated some lines in our database.yml configuration corresponding to our choice of PostgreSQL for database.
+It also generated some lines in our `database.yml` configuration corresponding to our choice of PostgreSQL for database.
NOTE. The only catch with using the SCM options is that you have to make your application's directory first, then initialize your SCM, then you can run the `rails new` command to generate the basis of your app.
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 1c720ad82f..c98e9b719c 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -62,12 +62,10 @@ These configuration methods are to be called on a `Rails::Railtie` object, such
* `config.autoload_once_paths` accepts an array of paths from which Rails will autoload constants that won't be wiped per request. Relevant if `config.cache_classes` is `false`, which is the case in development mode by default. Otherwise, all autoloading happens only once. All elements of this array must also be in `autoload_paths`. Default is an empty array.
-* `config.autoload_paths` accepts an array of paths from which Rails will autoload constants. Default is all directories under `app`.
+* `config.autoload_paths` accepts an array of paths from which Rails will autoload constants. Default is all directories under `app`. It is no longer recommended to adjust this. See [Autoloading and Reloading Constants](autoloading_and_reloading_constants.html#autoload-paths-and-eager-load-paths)
* `config.cache_classes` controls whether or not application classes and modules should be reloaded on each request. Defaults to `false` in development mode, and `true` in test and production modes.
-* `config.action_view.cache_template_loading` controls whether or not templates should be reloaded on each request. Defaults to whatever is set for `config.cache_classes`.
-
* `config.beginning_of_week` sets the default beginning of week for the
application. Accepts a valid week day symbol (e.g. `:monday`).
@@ -202,6 +200,7 @@ The full set of methods that can be used in this block are as follows:
* `force_plural` allows pluralized model names. Defaults to `false`.
* `helper` defines whether or not to generate helpers. Defaults to `true`.
* `integration_tool` defines which integration tool to use to generate integration tests. Defaults to `:test_unit`.
+* `system_tests` defines which integration tool to use to generate system tests. Defaults to `:test_unit`.
* `javascripts` turns on the hook for JavaScript files in generators. Used in Rails for when the `scaffold` generator is run. Defaults to `true`.
* `javascript_engine` configures the engine to be used (for eg. coffee) when generating assets. Defaults to `:js`.
* `orm` defines which orm to use. Defaults to `false` and will use Active Record by default.
@@ -322,6 +321,10 @@ All these configuration options are delegated to the `I18n` library.
* `config.active_record.schema_migrations_table_name` lets you set a string to be used as the name of the schema migrations table.
+* `config.active_record.internal_metadata_table_name` lets you set a string to be used as the name of the internal metadata table.
+
+* `config.active_record.protected_environments` lets you set an array of names of environments where destructive actions should be prohibited.
+
* `config.active_record.pluralize_table_names` specifies whether Rails will look for singular or plural table names in the database. If set to `true` (the default), then the Customer class will use the `customers` table. If set to false, then the Customer class will use the `customer` table.
* `config.active_record.default_timezone` determines whether to use `Time.local` (if set to `:local`) or `Time.utc` (if set to `:utc`) when pulling dates and times from the database. The default is `:utc`.
@@ -391,15 +394,21 @@ by setting up a Rake task which runs
```
for all models and all boolean columns, after which the flag must be set to true
-by adding the following to your application.rb file:
+by adding the following to your `application.rb` file:
```ruby
Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
```
-The schema dumper adds one additional configuration option:
+The schema dumper adds two additional configuration options:
+
+* `ActiveRecord::SchemaDumper.ignore_tables` accepts an array of tables that should _not_ be included in any generated schema file.
-* `ActiveRecord::SchemaDumper.ignore_tables` accepts an array of tables that should _not_ be included in any generated schema file. This setting is ignored unless `config.active_record.schema_format == :ruby`.
+* `ActiveRecord::SchemaDumper.fk_ignore_pattern` allows setting a different regular
+ expression that will be used to decide whether a foreign key's name should be
+ dumped to db/schema.rb or not. By default, foreign key names starting with
+ `fk_rails_` are not exported to the database schema dump.
+ Defaults to `/^fk_rails_[0-9a-f]{10}$/`.
### Configuring Action Controller
@@ -459,7 +468,10 @@ The schema dumper adds one additional configuration option:
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
- 'X-Content-Type-Options' => 'nosniff'
+ 'X-Content-Type-Options' => 'nosniff',
+ 'X-Download-Options' => 'noopen',
+ 'X-Permitted-Cross-Domain-Policies' => 'none',
+ 'Referrer-Policy' => 'strict-origin-when-cross-origin'
}
```
@@ -487,6 +499,19 @@ Defaults to `'signed cookie'`.
authenticated encrypted cookie salt. Defaults to `'authenticated encrypted
cookie'`.
+* `config.action_dispatch.encrypted_cookie_cipher` sets the cipher to be
+ used for encrypted cookies. This defaults to `"aes-256-gcm"`.
+
+* `config.action_dispatch.signed_cookie_digest` sets the digest to be
+ used for signed cookies. This defaults to `"SHA1"`.
+
+* `config.action_dispatch.cookies_rotations` allows rotating
+ secrets, ciphers, and digests for encrypted and signed cookies.
+
+* `config.action_dispatch.use_authenticated_cookie_encryption` controls whether
+ signed and encrypted cookies use the AES-256-GCM cipher or
+ the older AES-256-CBC cipher. It defaults to `true`.
+
* `config.action_dispatch.perform_deep_munge` configures whether `deep_munge`
method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation)
for more information. It defaults to `true`.
@@ -525,6 +550,8 @@ Defaults to `'signed cookie'`.
`config.action_view` includes a small number of configuration settings:
+* `config.action_view.cache_template_loading` controls whether or not templates should be reloaded on each request. Defaults to whatever is set for `config.cache_classes`.
+
* `config.action_view.field_error_proc` provides an HTML generator for displaying errors that come from Active Model. The default is
```ruby
@@ -563,12 +590,23 @@ Defaults to `'signed cookie'`.
error should be raised for missing translations.
* `config.action_view.automatically_disable_submit_tag` determines whether
- submit_tag should automatically disable on click, this defaults to `true`.
+ `submit_tag` should automatically disable on click, this defaults to `true`.
* `config.action_view.debug_missing_translation` determines whether to wrap the missing translations key in a `<span>` tag or not. This defaults to `true`.
* `config.action_view.form_with_generates_remote_forms` determines whether `form_with` generates remote forms or not. This defaults to `true`.
+* `config.action_view.form_with_generates_ids` determines whether `form_with` generates ids on inputs. This defaults to `true`.
+
+* `config.action_view.default_enforce_utf8` determines whether forms are generated with a hidden tag that forces older versions of Internet Explorer to submit forms encoded in UTF-8. This defaults to `false`.
+
+* `config.action_view.finalize_compiled_template_methods` determines
+ whether the methods on `ActionView::CompiledTemplates` that templates
+ compile themselves to are removed when template instances are
+ destroyed by the garbage collector. This helps prevent memory leaks in
+ development mode, but for large test suites, disabling this option in
+ the test environment can improve performance. This defaults to `true`.
+
### Configuring Action Mailer
There are a number of settings available on `config.action_mailer`:
@@ -657,6 +695,8 @@ 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`.
+* `config.active_support.use_sha1_digests` specifies whether to use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. Defaults to false.
+
* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`.
* `ActiveSupport::Cache::Store.logger` specifies the logger to use within cache store operations.
@@ -720,6 +760,8 @@ There are a few configuration options available in Active Support:
* `config.active_job.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Active Job. You can retrieve this logger by calling `logger` on either an Active Job class or an Active Job instance. Set to `nil` to disable logging.
+* `config.active_job.custom_serializers` allows to set custom argument serializers. Defaults to `[]`.
+
### Configuring Action Cable
* `config.action_cable.url` accepts a string for the URL for where
@@ -731,6 +773,43 @@ main application.
You can set this as nil to not mount Action Cable as part of your
normal Rails server.
+
+### Configuring Active Storage
+
+`config.active_storage` provides the following configuration options:
+
+* `config.active_storage.variant_processor` accepts a symbol `:mini_magick` or `:vips`, specifying whether variant transformations will be performed with MiniMagick or ruby-vips. The default is `:mini_magick`.
+
+* `config.active_storage.analyzers` accepts an array of classes indicating the analyzers available for Active Storage blobs. The default is `[ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer]`. The former can extract width and height of an image blob; the latter can extract width, height, duration, angle, and aspect ratio of a video blob.
+
+* `config.active_storage.previewers` accepts an array of classes indicating the image previewers available in Active Storage blobs. The default is `[ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer]`. The former can generate a thumbnail from the first page of a PDF blob; the latter from the relevant frame of a video blob.
+
+* `config.active_storage.paths` accepts a hash of options indicating the locations of previewer/analyzer commands. The default is `{}`, meaning the commands will be looked for in the default path. Can include any of these options:
+ * `:ffprobe` - The location of the ffprobe executable.
+ * `:mutool` - The location of the mutool executable.
+ * `:ffmpeg` - The location of the ffmpeg executable.
+
+ ```ruby
+ config.active_storage.paths[:ffprobe] = '/usr/local/bin/ffprobe'
+ ```
+
+* `config.active_storage.variable_content_types` accepts an array of strings indicating the content types that Active Storage can transform through ImageMagick. The default is `%w(image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop)`.
+
+* `config.active_storage.content_types_to_serve_as_binary` accepts an array of strings indicating the content types that Active Storage will always serve as an attachment, rather than inline. The default is `%w(text/html
+text/javascript image/svg+xml application/postscript application/x-shockwave-flash text/xml application/xml application/xhtml+xml)`.
+
+* `config.active_storage.queue` can be used to set the name of the Active Job queue used to perform jobs like analyzing the content of a blob or purging a blog.
+
+ ```ruby
+ config.active_job.queue = :low_priority
+ ```
+
+* `config.active_storage.logger` can be used to set the logger used by Active Storage. Accepts a logger conforming to the interface of Log4r or the default Ruby Logger class.
+
+ ```ruby
+ config.active_job.logger = ActiveSupport::Logger.new(STDOUT)
+ ```
+
### Configuring a Database
Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`.
@@ -964,7 +1043,7 @@ By default Rails ships with three environments: "development", "test", and "prod
Imagine you have a server which mirrors the production environment but is only used for testing. Such a server is commonly called a "staging server". To define an environment called "staging" for this server, just create a file called `config/environments/staging.rb`. Please use the contents of any existing file in `config/environments` as a starting point and make the necessary changes from there.
-That environment is no different than the default ones, start a server with `rails server -e staging`, a console with `rails console staging`, `Rails.env.staging?` works, etc.
+That environment is no different than the default ones, start a server with `rails server -e staging`, a console with `rails console -e staging`, `Rails.env.staging?` works, etc.
### Deploy to a subdirectory (relative url root)
@@ -994,11 +1073,11 @@ Deploying your application using a reverse proxy has definite advantages over tr
Many modern web servers can be used as a proxy server to balance third-party elements such as caching servers or application servers.
-One such application server you can use is [Unicorn](http://unicorn.bogomips.org/) to run behind a reverse proxy.
+One such application server you can use is [Unicorn](https://bogomips.org/unicorn/) to run behind a reverse proxy.
In this case, you would need to configure the proxy server (NGINX, Apache, etc) to accept connections from your application server (Unicorn). By default Unicorn will listen for TCP connections on port 8080, but you can change the port or configure it to use sockets instead.
-You can find more information in the [Unicorn readme](http://unicorn.bogomips.org/README.html) and understand the [philosophy](http://unicorn.bogomips.org/PHILOSOPHY.html) behind it.
+You can find more information in the [Unicorn readme](https://bogomips.org/unicorn/README.html) and understand the [philosophy](https://bogomips.org/unicorn/PHILOSOPHY.html) behind it.
Once you've configured the application server, you must proxy requests to it by configuring your web server appropriately. For example your NGINX config may include:
@@ -1026,7 +1105,7 @@ server {
}
```
-Be sure to read the [NGINX documentation](http://nginx.org/en/docs/) for the most up-to-date information.
+Be sure to read the [NGINX documentation](https://nginx.org/en/docs/) for the most up-to-date information.
Rails Environment Settings
@@ -1048,7 +1127,7 @@ After loading the framework and any gems in your application, Rails turns to loa
NOTE: You can use subfolders to organize your initializers if you like, because Rails will look into the whole file hierarchy from the initializers folder on down.
-TIP: If you have any ordering dependency in your initializers, you can control the load order through naming. Initializer files are loaded in alphabetical order by their path. For example, `01_critical.rb` will be loaded before `02_normal.rb`.
+TIP: While Rails supports numbering of initializer file names for load ordering purposes, a better technique is to place any code that need to load in a specific order within the same file. This reduces file name churn, makes dependencies more explicit, and can help surface new concepts within your application.
Initialization events
---------------------
@@ -1308,7 +1387,7 @@ know which pages it is allowed to index.
Rails creates this file for you inside the `/public` folder. By default, it allows
search engines to index all pages of your application. If you want to block
-indexing on all pages of you application, use this:
+indexing on all pages of your application, use this:
```
User-agent: *
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index 7424818757..c1668f989b 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -84,7 +84,9 @@ discussions new features require.
Helping to Resolve Existing Issues
----------------------------------
-As a next step beyond reporting issues, you can help the core team resolve existing issues. If you check the [issues list](https://github.com/rails/rails/issues) in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually:
+As a next step beyond reporting issues, you can help the core team resolve existing ones by providing feedback about them. If you are new to Rails core development, that might be a great way to walk your first steps, you'll get familiar with the code base and the processes.
+
+If you check the [issues list](https://github.com/rails/rails/issues) in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually:
### Verifying Bug Reports
@@ -132,7 +134,8 @@ learn about Ruby on Rails, and the API, which serves as a reference.
You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing them up to date with the latest edge Rails.
-To do so, open a pull request to [Rails](https://github.com/rails/rails) on GitHub.
+To do so, make changes to Rails guides source files (located [here](https://github.com/rails/rails/tree/master/guides/source) on GitHub). Then open a pull request to apply your
+changes to master branch.
When working with documentation, please take into account the [API Documentation Guidelines](api_documentation_guidelines.html) and the [Ruby on Rails Guides Guidelines](ruby_on_rails_guides_guidelines.html).
diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb
deleted file mode 100644
index 5adbd12ac0..0000000000
--- a/guides/source/credits.html.erb
+++ /dev/null
@@ -1,80 +0,0 @@
-<% content_for :page_title do %>
-Ruby on Rails Guides: Credits
-<% end %>
-
-<% content_for :header_section do %>
-<h2>Credits</h2>
-
-<p>We'd like to thank the following people for their tireless contributions to this project.</p>
-
-<% end %>
-
-<h3 class="section">Rails Guides Reviewers</h3>
-
-<%= author('Vijay Dev', 'vijaydev', 'vijaydev.jpg') do %>
- Vijayakumar, found as Vijay Dev on the web, is a web applications developer and an open source enthusiast who lives in Chennai, India. He started using Rails in 2009 and began actively contributing to Rails documentation in late 2010. He <a href="https://twitter.com/vijay_dev">tweets</a> a lot and also <a href="http://vijaydev.wordpress.com">blogs</a>.
-<% end %>
-
-<%= author('Xavier Noria', 'fxn', 'fxn.png') do %>
- Xavier Noria has been into Ruby on Rails since 2005. He is a Rails core team member and enjoys combining his passion for Rails and his past life as a proofreader of math textbooks. Xavier is currently an independent Ruby on Rails consultant. Oh, he also <a href="http://twitter.com/fxn">tweets</a> and can be found everywhere as &quot;fxn&quot;.
-<% end %>
-
-<h3 class="section">Rails Guides Designers</h3>
-
-<%= author('Jason Zimdars', 'jz') do %>
- Jason Zimdars is an experienced creative director and web designer who has lead UI and UX design for numerous websites and web applications. You can see more of his design and writing at <a href="http://www.thinkcage.com/">Thinkcage.com</a> or follow him on <a href="https://twitter.com/jasonzimdars">Twitter</a>.
-<% end %>
-
-<h3 class="section">Rails Guides Authors</h3>
-
-<%= author('Ryan Bigg', 'radar', 'radar.png') do %>
- Ryan Bigg works as a Rails developer at <a href="http://marketplacer.com">Marketplacer</a> and has been working with Rails since 2006. He's the author of <a href="https://leanpub.com/multi-tenancy-rails">Multi Tenancy With Rails</a> and co-author of <a href="http://manning.com/bigg2">Rails 4 in Action</a>. He's written many gems which can be seen on <a href="https://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>.
-<% end %>
-
-<%= author('Oscar Del Ben', 'oscardelben', 'oscardelben.jpg') do %>
-Oscar Del Ben is a software engineer at <a href="http://www.businessinsider.com/google-buys-wildfire-2012-8">Wildfire</a>. He's a regular open source contributor (<a href="https://github.com/oscardelben">GitHub account</a>) and tweets regularly at <a href="https://twitter.com/oscardelben">@oscardelben</a>.
- <% end %>
-
-<%= author('Frederick Cheung', 'fcheung') do %>
- Frederick Cheung is Chief Wizard at Texperts where he has been using Rails since 2006. He is based in Cambridge (UK) and when not consuming fine ales he blogs at <a href="http://www.spacevatican.org">spacevatican.org</a>.
-<% end %>
-
-<%= author('Tore Darell', 'toretore') do %>
- Tore Darell is an independent developer based in Menton, France who specialises in cruft-free web applications using Ruby, Rails and unobtrusive JavaScript. You can follow him on <a href="http://twitter.com/toretore">Twitter</a>.
-<% end %>
-
-<%= author('Jeff Dean', 'zilkey') do %>
- Jeff Dean is a software engineer with <a href="http://pivotallabs.com">Pivotal Labs</a>.
-<% end %>
-
-<%= author('Mike Gunderloy', 'mgunderloy') do %>
- Mike Gunderloy is a consultant with <a href="http://www.actionrails.com">ActionRails</a>. He brings 25 years of experience in a variety of languages to bear on his current work with Rails. His near-daily links and other blogging can be found at <a href="http://afreshcup.com">A Fresh Cup</a> and he <a href="http://twitter.com/MikeG1">twitters</a> too much.
-<% end %>
-
-<%= author('Mikel Lindsaar', 'raasdnil') do %>
- Mikel Lindsaar has been working with Rails since 2006 and is the author of the Ruby <a href="https://github.com/mikel/mail">Mail gem</a> and core contributor (he helped re-write Action Mailer's API). Mikel is the founder of <a href="http://rubyx.com/">RubyX</a>, has a <a href="http://lindsaar.net/">blog</a> and <a href="http://twitter.com/raasdnil">tweets</a>.
-<% end %>
-
-<%= author('Cássio Marques', 'cmarques') do %>
- Cássio Marques is a Brazilian software developer working with different programming languages such as Ruby, JavaScript, CPP and Java, as an independent consultant. He blogs at <a href="http://cassiomarques.wordpress.com">/* CODIFICANDO */</a>, which is mainly written in Portuguese, but will soon get a new section for posts with English translation.
-<% end %>
-
-<%= author('James Miller', 'bensie') do %>
- James Miller is a software developer for <a href="http://www.jk-tech.com">JK Tech</a> in San Diego, CA. You can find James on GitHub, Gmail, Twitter, and Freenode as &quot;bensie&quot;.
-<% end %>
-
-<%= author('Pratik Naik', 'lifo') do %>
- Pratik Naik is a Ruby on Rails developer at <a href="https://basecamp.com/">Basecamp</a> and maintains a blog at <a href="http://m.onkey.org">has_many :bugs, :through =&gt; :rails</a>. He also has a semi-active <a href="http://twitter.com/lifo">twitter account</a>.
-<% end %>
-
-<%= author('Emilio Tagua', 'miloops') do %>
- Emilio Tagua &mdash;a.k.a. miloops&mdash; is an Argentinian entrepreneur, developer, open source contributor and Rails evangelist. Cofounder of <a href="http://eventioz.com">Eventioz</a>. He has been using Rails since 2006 and contributing since early 2008. Can be found at gmail, twitter, freenode, everywhere as &quot;miloops&quot;.
-<% end %>
-
-<%= author('Heiko Webers', 'hawe') do %>
- Heiko Webers is the founder of <a href="http://www.bauland42.de">bauland42</a>, a German web application security consulting and development company focused on Ruby on Rails. He blogs at the <a href="http://www.rorsecurity.info">Ruby on Rails Security Project</a>. After 10 years of desktop application development, Heiko has rarely looked back.
-<% end %>
-
-<%= author('Akshay Surve', 'startupjockey', 'akshaysurve.jpg') do %>
- Akshay Surve is the Founder at <a href="http://www.deltax.com">DeltaX</a>, hackathon specialist, a midnight code junkie and occasionally writes prose. You can connect with him on <a href="https://twitter.com/akshaysurve">Twitter</a>, <a href="http://www.linkedin.com/in/akshaysurve">Linkedin</a>, <a href="http://www.akshaysurve.com/">Personal Blog</a> or <a href="http://www.quora.com/Akshay-Surve">Quora</a>.
-<% end %>
diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml
index 59205ee465..5cddf79eeb 100644
--- a/guides/source/documents.yaml
+++ b/guides/source/documents.yaml
@@ -72,7 +72,7 @@
url: active_support_core_extensions.html
description: This guide documents the Ruby core extensions defined in Active Support.
-
- name: Rails Internationalization API
+ name: Rails Internationalization (I18n) API
url: i18n.html
description: This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country, and so on.
-
@@ -84,6 +84,10 @@
url: active_job_basics.html
description: This guide provides you with all you need to get started creating, enqueuing, and executing background jobs.
-
+ name: Active Storage Overview
+ url: active_storage_overview.html
+ description: This guide covers how to attach files to your Active Record models.
+ -
name: Testing Rails Applications
url: testing.html
description: This is a rather comprehensive guide to the various testing facilities in Rails. It covers everything from 'What is a test?' to Integration Testing. Enjoy.
@@ -104,7 +108,7 @@
url: command_line.html
description: This guide covers the command line tools provided by Rails.
-
- name: Asset Pipeline
+ name: The Asset Pipeline
url: asset_pipeline.html
description: This guide documents the asset pipeline.
-
@@ -151,7 +155,7 @@
url: rails_on_rack.html
description: This guide covers Rails integration with Rack and interfacing with other Rack components.
-
- name: Creating and Customizing Rails Generators
+ name: Creating and Customizing Rails Generators & Templates
url: generators.html
description: This guide covers the process of adding a brand new generator to your extension or providing an alternative to an element of a built-in Rails generator (such as providing alternative test stubs for the scaffold generator).
-
@@ -159,6 +163,11 @@
url: engines.html
description: This guide explains how to write a mountable engine.
work_in_progress: true
+ -
+ name: Threading and Code Execution in Rails
+ url: threading_and_code_execution.html
+ description: This guide describes the considerations needed and tools available when working directly with concurrency in a Rails application.
+ work_in_progress: true
-
name: Contributing to Ruby on Rails
documents:
@@ -178,7 +187,7 @@
name: Maintenance Policy
documents:
-
- name: Maintenance Policy
+ name: Maintenance Policy for Ruby on Rails
url: maintenance_policy.html
description: What versions of Ruby on Rails are currently supported, and when to expect new versions.
-
@@ -189,6 +198,10 @@
url: upgrading_ruby_on_rails.html
description: This guide helps in upgrading applications to latest Ruby on Rails versions.
-
+ name: Ruby on Rails 5.2 Release Notes
+ url: 5_2_release_notes.html
+ description: Release notes for Rails 5.2.
+ -
name: Ruby on Rails 5.1 Release Notes
url: 5_1_release_notes.html
description: Release notes for Rails 5.1.
diff --git a/guides/source/engines.md b/guides/source/engines.md
index c7331b6ca4..8d81296fa5 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -63,7 +63,7 @@ authentication for its parent applications, or
[Thredded](https://github.com/thredded/thredded), an engine that provides forum
functionality. There's also [Spree](https://github.com/spree/spree) which
provides an e-commerce platform, and
-[RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine.
+[Refinery CMS](https://github.com/refinery/refinerycms), a CMS engine.
Finally, engines would not have been possible without the work of James Adam,
Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever
@@ -537,12 +537,12 @@ directory at `app/views/blorgh/comments` and in it a new file called
```html+erb
<h3>New comment</h3>
-<%= form_for [@article, @article.comments.build] do |f| %>
+<%= form_with(model: [@article, @article.comments.build], local: true) do |form| %>
<p>
- <%= f.label :text %><br>
- <%= f.text_area :text %>
+ <%= form.label :text %><br>
+ <%= form.text_area :text %>
</p>
- <%= f.submit %>
+ <%= form.submit %>
<% end %>
```
@@ -653,7 +653,7 @@ there isn't an application handy to test this out in, generate one using the
$ rails new unicorn
```
-Usually, specifying the engine inside the Gemfile would be done by specifying it
+Usually, specifying the engine inside the `Gemfile` would be done by specifying it
as a normal, everyday gem.
```ruby
@@ -783,8 +783,8 @@ added above the `title` field with this code:
```html+erb
<div class="field">
- <%= f.label :author_name %><br>
- <%= f.text_field :author_name %>
+ <%= form.label :author_name %><br>
+ <%= form.text_field :author_name %>
</div>
```
@@ -921,7 +921,7 @@ engine:
mattr_accessor :author_class
```
-This method works like its brothers, `attr_accessor` and `cattr_accessor`, but
+This method works like its siblings, `attr_accessor` and `cattr_accessor`, but
provides a setter and getter method on the module with the specified name. To
use it, it must be referenced using `Blorgh.author_class`.
@@ -982,7 +982,7 @@ Blorgh.author_class = "User"
WARNING: It's very important here to use the `String` version of the class,
rather than the class itself. If you were to use the class, Rails would attempt
to load that class and then reference the related table. This could lead to
-problems if the table wasn't already existing. Therefore, a `String` should be
+problems if the table didn't already exist. Therefore, a `String` should be
used and then converted to a class using `constantize` in the engine later on.
Go ahead and try to create a new article. You will see that it works exactly in the
@@ -1322,7 +1322,7 @@ engine.
Assets within an engine work in an identical way to a full application. Because
the engine class inherits from `Rails::Engine`, the application will know to
-look up assets in the engine's 'app/assets' and 'lib/assets' directories.
+look up assets in the engine's `app/assets` and `lib/assets` directories.
Like all of the other components of an engine, the assets should be namespaced.
This means that if you have an asset called `style.css`, it should be placed at
@@ -1361,7 +1361,7 @@ that only exists for your engine. In this case, the host application doesn't
need to require `admin.css` or `admin.js`. Only the gem's admin layout needs
these assets. It doesn't make sense for the host app to include
`"blorgh/admin.css"` in its stylesheets. In this situation, you should
-explicitly define these assets for precompilation. This tells sprockets to add
+explicitly define these assets for precompilation. This tells Sprockets to add
your engine assets when `bin/rails assets:precompile` is triggered.
You can define assets for precompilation in `engine.rb`:
@@ -1501,6 +1501,7 @@ To hook into the initialization process of one of the following classes use the
| `ActionController::Base` | `action_controller` |
| `ActionController::TestCase` | `action_controller_test_case` |
| `ActionDispatch::IntegrationTest` | `action_dispatch_integration_test` |
+| `ActionDispatch::SystemTestCase` | `action_dispatch_system_test_case` |
| `ActionMailer::Base` | `action_mailer` |
| `ActionMailer::TestCase` | `action_mailer_test_case` |
| `ActionView::Base` | `action_view` |
@@ -1513,14 +1514,14 @@ To hook into the initialization process of one of the following classes use the
## Configuration hooks
-These are the available configuration hooks. They do not hook into any particular framework, instead they run in context of the entire application.
+These are the available configuration hooks. They do not hook into any particular framework, but instead they run in context of the entire application.
-| Hook | Use Case |
-| ---------------------- | ------------------------------------------------------------------------------------- |
-| `before_configuration` | First configurable block to run. Called before any initializers are run. |
-| `before_initialize` | Second configurable block to run. Called before frameworks initialize. |
-| `before_eager_load` | Third configurable block to run. Does not run if `config.cache_classes` set to false. |
-| `after_initialize` | Last configurable block to run. Called after frameworks initialize. |
+| Hook | Use Case |
+| ---------------------- | ---------------------------------------------------------------------------------- |
+| `before_configuration` | First configurable block to run. Called before any initializers are run. |
+| `before_initialize` | Second configurable block to run. Called before frameworks initialize. |
+| `before_eager_load` | Third configurable block to run. Does not run if `config.eager_load` set to false. |
+| `after_initialize` | Last configurable block to run. Called after frameworks initialize. |
### Example
diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md
index f46f1648b3..53c567727f 100644
--- a/guides/source/form_helpers.md
+++ b/guides/source/form_helpers.md
@@ -1,7 +1,7 @@
**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
-Form Helpers
-============
+Action View Form Helpers
+========================
Forms in web applications are an essential interface for user input. However, form markup can quickly become tedious to write and maintain because of the need to handle form control naming and its numerous attributes. Rails does away with this complexity by providing view helpers for generating form markup. However, since these helpers have different use cases, developers need to know the differences between the helper methods before putting them to use.
@@ -274,10 +274,12 @@ There are a few things to note here:
The resulting HTML is:
```html
-<form accept-charset="UTF-8" action="/articles" method="post" class="nifty_form">
- <input id="article_title" name="article[title]" type="text" />
- <textarea id="article_body" name="article[body]" cols="60" rows="12"></textarea>
- <input name="commit" type="submit" value="Create" />
+<form class="nifty_form" id="new_article" action="/articles" accept-charset="UTF-8" method="post">
+ <input name="utf8" type="hidden" value="&#x2713;" />
+ <input type="hidden" name="authenticity_token" value="NRkFyRWxdYNfUg7vYxLOp2SLf93lvnl+QwDWorR42Dp6yZXPhHEb6arhDOIWcqGit8jfnrPwL781/xlrzj63TA==" />
+ <input type="text" name="article[title]" id="article_title" />
+ <textarea name="article[body]" id="article_body" cols="60" rows="12"></textarea>
+ <input type="submit" name="commit" value="Create" data-disable-with="Create" />
</form>
```
@@ -299,9 +301,11 @@ You can create a similar binding without actually creating `<form>` tags with th
which produces the following output:
```html
-<form accept-charset="UTF-8" action="/people" class="new_person" id="new_person" method="post">
- <input id="person_name" name="person[name]" type="text" />
- <input id="contact_detail_phone_number" name="contact_detail[phone_number]" type="text" />
+<form class="new_person" id="new_person" action="/people" accept-charset="UTF-8" method="post">
+ <input name="utf8" type="hidden" value="&#x2713;" />
+ <input type="hidden" name="authenticity_token" value="bL13x72pldyDD8bgtkjKQakJCpd4A8JdXGbfksxBDHdf1uC0kCMqe2tvVdUYfidJt0fj3ihC4NxiVHv8GVYxJA==" />
+ <input type="text" name="person[name]" id="person_name" />
+ <input type="text" name="contact_detail[phone_number]" id="contact_detail_phone_number" />
</form>
```
@@ -916,7 +920,7 @@ When an association accepts nested attributes `fields_for` renders its block onc
```ruby
def new
@person = Person.new
- 2.times { @person.addresses.build}
+ 2.times { @person.addresses.build }
end
```
diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md
index 7c7b3a4c01..5b6cfe6659 100644
--- a/guides/source/getting_started.md
+++ b/guides/source/getting_started.md
@@ -87,10 +87,10 @@ current version of Ruby installed:
```bash
$ ruby -v
-ruby 2.3.1p112
+ruby 2.5.0
```
-Rails requires Ruby version 2.2.2 or later. If the version number returned is
+Rails requires Ruby version 2.4.1 or later. If the version number returned is
less than that number, you'll need to install a fresh copy of Ruby.
TIP: A number of tools exist to help you quickly install Ruby and Ruby
@@ -100,7 +100,7 @@ For more installation methods for most Operating Systems take a look at
[ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/).
If you are working on Windows, you should also install the
-[Ruby Installer Development Kit](http://rubyinstaller.org/downloads/).
+[Ruby Installer Development Kit](https://rubyinstaller.org/downloads/).
You will also need an installation of the SQLite3 database.
Many popular UNIX-like OSes ship with an acceptable version of SQLite3.
@@ -170,16 +170,16 @@ of the files and folders that Rails created by default:
| File/Folder | Purpose |
| ----------- | ------- |
|app/|Contains the controllers, models, views, helpers, mailers, channels, jobs and assets for your application. You'll focus on this folder for the remainder of this guide.|
-|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, update, deploy or run your application.|
+|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, update, deploy, or run your application.|
|config/|Configure your application's routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html).|
-|config.ru|Rack configuration for Rack based servers used to start the application.|
+|config.ru|Rack configuration for Rack based servers used to start the application. For more information about Rack, see the [Rack website](https://rack.github.io/).|
|db/|Contains your current database schema, as well as the database migrations.|
|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see the [Bundler website](https://bundler.io).|
|lib/|Extended modules for your application.|
|log/|Application log files.|
|package.json|This file allows you to specify what npm dependencies are needed for your Rails application. This file is used by Yarn. For more information about Yarn, see the [Yarn website](https://yarnpkg.com/lang/en/).|
|public/|The only folder seen by the world as-is. Contains static files and compiled assets.|
-|Rakefile|This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing Rakefile, you should add your own tasks by adding files to the `lib/tasks` directory of your application.|
+|Rakefile|This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing `Rakefile`, you should add your own tasks by adding files to the `lib/tasks` directory of your application.|
|README.md|This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.|
|test/|Unit tests, fixtures, and other test apparatus. These are covered in [Testing Rails Applications](testing.html).|
|tmp/|Temporary files (like cache and pid files).|
@@ -346,9 +346,9 @@ Now that you've seen how to create a controller, an action and a view, let's
create something with a bit more substance.
In the Blog application, you will now create a new _resource_. A resource is the
-term used for a collection of similar objects, such as articles, people or
+term used for a collection of similar objects, such as articles, people, or
animals.
-You can create, read, update and destroy items for a resource and these
+You can create, read, update, and destroy items for a resource and these
operations are referred to as _CRUD_ operations.
Rails provides a `resources` method which can be used to declare a standard REST
@@ -372,16 +372,17 @@ singular form `article` and makes meaningful use of the distinction.
```bash
$ bin/rails routes
- Prefix Verb URI Pattern Controller#Action
- articles GET /articles(.:format) articles#index
- POST /articles(.:format) articles#create
- new_article GET /articles/new(.:format) articles#new
-edit_article GET /articles/:id/edit(.:format) articles#edit
- article GET /articles/:id(.:format) articles#show
- PATCH /articles/:id(.:format) articles#update
- PUT /articles/:id(.:format) articles#update
- DELETE /articles/:id(.:format) articles#destroy
- root GET / welcome#index
+ Prefix Verb URI Pattern Controller#Action
+welcome_index GET /welcome/index(.:format) welcome#index
+ articles GET /articles(.:format) articles#index
+ POST /articles(.:format) articles#create
+ new_article GET /articles/new(.:format) articles#new
+ edit_article GET /articles/:id/edit(.:format) articles#edit
+ article GET /articles/:id(.:format) articles#show
+ PATCH /articles/:id(.:format) articles#update
+ PUT /articles/:id(.:format) articles#update
+ DELETE /articles/:id(.:format) articles#destroy
+ root GET / welcome#index
```
In the next section, you will add the ability to create new articles in your
@@ -461,8 +462,7 @@ You're getting this error now because Rails expects plain actions like this one
to have views associated with them to display their information. With no view
available, Rails will raise an exception.
-In the above image, the bottom line has been truncated. Let's see what the full
-error message looks like:
+Let's look at the full error message again:
>ArticlesController#new is missing a template for this request format and variant. request.formats: ["text/html"] request.variant: [] NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not… nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot.
@@ -504,7 +504,7 @@ write this content in it:
```
When you refresh <http://localhost:3000/articles/new> you'll now see that the
-page has a title. The route, controller, action and view are now working
+page has a title. The route, controller, action, and view are now working
harmoniously! It's time to create the form for a new article.
### The first form
@@ -567,15 +567,16 @@ To see what Rails will do with this, we look back at the output of
```bash
$ bin/rails routes
Prefix Verb URI Pattern Controller#Action
- articles GET /articles(.:format) articles#index
- POST /articles(.:format) articles#create
- new_article GET /articles/new(.:format) articles#new
-edit_article GET /articles/:id/edit(.:format) articles#edit
- article GET /articles/:id(.:format) articles#show
- PATCH /articles/:id(.:format) articles#update
- PUT /articles/:id(.:format) articles#update
- DELETE /articles/:id(.:format) articles#destroy
- root GET / welcome#index
+welcome_index GET /welcome/index(.:format) welcome#index
+ articles GET /articles(.:format) articles#index
+ POST /articles(.:format) articles#create
+ new_article GET /articles/new(.:format) articles#new
+ edit_article GET /articles/:id/edit(.:format) articles#edit
+ article GET /articles/:id(.:format) articles#show
+ PATCH /articles/:id(.:format) articles#update
+ PUT /articles/:id(.:format) articles#update
+ DELETE /articles/:id(.:format) articles#destroy
+ root GET / welcome#index
```
The `articles_path` helper tells Rails to point the form to the URI Pattern
@@ -594,7 +595,7 @@ familiar error:
You now need to create the `create` action within the `ArticlesController` for
this to work.
-NOTE: by default `form_with` submits forms using Ajax thereby skipping full page
+NOTE: By default `form_with` submits forms using Ajax thereby skipping full page
redirects. To make this guide easier to get into we've disabled that with
`local: true` for now.
@@ -1121,10 +1122,10 @@ that otherwise `@article` would be `nil` in our view, and calling
`@article.errors.any?` would throw an error.
TIP: Rails automatically wraps fields that contain an error with a div
-with class `field_with_errors`. You can define a css rule to make them
+with class `field_with_errors`. You can define a CSS rule to make them
standout.
-Now you'll get a nice error message when saving an article without title when
+Now you'll get a nice error message when saving an article without a title when
you attempt to do just that on the new article form
<http://localhost:3000/articles/new>:
@@ -1203,10 +1204,10 @@ it look as follows:
This time we point the form to the `update` action, which is not defined yet
but will be very soon.
-Passing the article object to the method, will automagically create url for submitting the edited article form.
-This option tells Rails that we want this form to be submitted
-via the `PATCH` HTTP method which is the HTTP method you're expected to use to
-**update** resources according to the REST protocol.
+Passing the article object to the method will automatically set the URL for
+submitting the edited article form. This option tells Rails that we want this
+form to be submitted via the `PATCH` HTTP method, which is the HTTP method you're
+expected to use to **update** resources according to the REST protocol.
The arguments to `form_with` could be model objects, say, `model: @article` which would
cause the helper to fill in the form with the fields of the object. Passing in a
@@ -1505,7 +1506,7 @@ appear.
TIP: Learn more about Unobtrusive JavaScript on
[Working With JavaScript in Rails](working_with_javascript_in_rails.html) guide.
-Congratulations, you can now create, show, list, update and destroy
+Congratulations, you can now create, show, list, update, and destroy
articles.
TIP: In general, Rails encourages using resources objects instead of
@@ -1521,7 +1522,7 @@ comments on articles.
### Generating a Model
We're going to see the same generator that we used before when creating
-the `Article` model. This time we'll create a `Comment` model to hold
+the `Article` model. This time we'll create a `Comment` model to hold a
reference to an article. Run this command in your terminal:
```bash
@@ -1856,7 +1857,7 @@ This will now render the partial in `app/views/comments/_comment.html.erb` once
for each comment that is in the `@article.comments` collection. As the `render`
method iterates over the `@article.comments` collection, it assigns each
comment to a local variable named the same as the partial, in this case
-`comment` which is then available in the partial for us to show.
+`comment`, which is then available in the partial for us to show.
### Rendering a Partial Form
@@ -2059,13 +2060,13 @@ What's Next?
Now that you've seen your first Rails application, you should feel free to
update it and experiment on your own.
-Remember you don't have to do everything without help. As you need assistance
+Remember, you don't have to do everything without help. As you need assistance
getting up and running with Rails, feel free to consult these support
resources:
* The [Ruby on Rails Guides](index.html)
-* The [Ruby on Rails Tutorial](http://railstutorial.org/book)
-* The [Ruby on Rails mailing list](http://groups.google.com/group/rubyonrails-talk)
+* The [Ruby on Rails Tutorial](https://www.railstutorial.org/book)
+* The [Ruby on Rails mailing list](https://groups.google.com/group/rubyonrails-talk)
* The [#rubyonrails](irc://irc.freenode.net/#rubyonrails) channel on irc.freenode.net
diff --git a/guides/source/i18n.md b/guides/source/i18n.md
index cb24822f86..f42ab15b8b 100644
--- a/guides/source/i18n.md
+++ b/guides/source/i18n.md
@@ -42,6 +42,8 @@ Internationalization is a complex problem. Natural languages differ in so many w
As part of this solution, **every static string in the Rails framework** - e.g. Active Record validation messages, time and date formats - **has been internationalized**. _Localization_ of a Rails application means defining translated values for these strings in desired languages.
+To localize store and update _content_ in your application (e.g. translate blog posts), see the [Translating model content](#translating-model-content) section.
+
### The Overall Architecture of the Library
Thus, the Ruby I18n gem is split into two parts:
@@ -105,7 +107,7 @@ This means, that in the `:en` locale, the key _hello_ will map to the _Hello wor
The I18n library will use **English** as a **default locale**, i.e. if a different locale is not set, `:en` will be used for looking up translations.
-NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](https://groups.google.com/forum/#!topic/rails-i18n/FN7eLH2-lHA)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Few gems such as [Globalize3](https://github.com/globalize/globalize) may help you implement it.
+NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](https://groups.google.com/forum/#!topic/rails-i18n/FN7eLH2-lHA)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary.
The **translations load path** (`I18n.load_path`) is an array of paths to files that will be loaded automatically. Configuring this path allows for customization of translations directory structure and file naming scheme.
@@ -827,14 +829,14 @@ For example when you add the following translations:
en:
activerecord:
models:
- user: Dude
+ user: Customer
attributes:
user:
login: "Handle"
# will translate User attribute "login" as "Handle"
```
-Then `User.model_name.human` will return "Dude" and `User.human_attribute_name("login")` will return "Handle".
+Then `User.model_name.human` will return "Customer" and `User.human_attribute_name("login")` will return "Handle".
You can also set a plural form for model names, adding as following:
@@ -843,11 +845,11 @@ en:
activerecord:
models:
user:
- one: Dude
- other: Dudes
+ one: Customer
+ other: Customers
```
-Then `User.model_name.human(count: 2)` will return "Dudes". With `count: 1` or without params will return "Dude".
+Then `User.model_name.human(count: 2)` will return "Customers". With `count: 1` or without params will return "Customer".
In the event you need to access nested attributes within a given model, you should nest these under `model/attribute` at the model level of your translation file:
@@ -855,12 +857,12 @@ In the event you need to access nested attributes within a given model, you shou
en:
activerecord:
attributes:
- user/gender:
- female: "Female"
- male: "Male"
+ user/role:
+ admin: "Admin"
+ contributor: "Contributor"
```
-Then `User.human_attribute_name("gender.female")` will return "Female".
+Then `User.human_attribute_name("role.admin")` will return "Admin".
NOTE: If you are using a class which includes `ActiveModel` and does not inherit from `ActiveRecord::Base`, replace `activerecord` with `activemodel` in the above key paths.
@@ -977,7 +979,7 @@ en:
```
NOTE: In order to use this helper, you need to install [DynamicForm](https://github.com/joelmoss/dynamic_form)
-gem by adding this line to your Gemfile: `gem 'dynamic_form'`.
+gem by adding this line to your `Gemfile`: `gem 'dynamic_form'`.
### Translations for Action Mailer E-Mail Subjects
@@ -1099,13 +1101,11 @@ Customize your I18n Setup
For several reasons the Simple backend shipped with Active Support only does the "simplest thing that could possibly work" _for Ruby on Rails_[^3] ... which means that it is only guaranteed to work for English and, as a side effect, languages that are very similar to English. Also, the simple backend is only capable of reading translations but cannot dynamically store them to any format.
-That does not mean you're stuck with these limitations, though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs. E.g. you could exchange it with Globalize's Static backend:
+That does not mean you're stuck with these limitations, though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs, by passing a backend instance to the `I18n.backend=` setter.
-```ruby
-I18n.backend = Globalize::Backend::Static.new
-```
+For example, you can replace the Simple backend with the the Chain backend to chain multiple backends together. This is useful when you want to use standard translations with a Simple backend but store custom application translations in a database or other backends.
-You can also use the Chain backend to chain multiple backends together. This is useful when you want to use standard translations with a Simple backend but store custom application translations in a database or other backends. For example, you could use the Active Record backend and fall back to the (default) Simple backend:
+With the Chain backend, you could use the Active Record backend and fall back to the (default) Simple backend:
```ruby
I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
@@ -1166,20 +1166,29 @@ To do so, the helper forces `I18n#translate` to raise exceptions no matter what
I18n.t :foo, raise: true # always re-raises exceptions from the backend
```
+Translating Model Content
+-------------------------
+
+The I18n API described in this guide is primarily intended for translating interface strings. If you are looking to translate model content (e.g. blog posts), you will need a different solution to help with this.
+
+Several gems can help with this:
+
+* [Globalize](https://github.com/globalize/globalize): Store translations on separate translation tables, one for each translated model
+* [Mobility](https://github.com/shioyama/mobility): Provides support for storing translations in many formats, including translation tables, json columns (Postgres), etc.
+* [Traco](https://github.com/barsoom/traco): Translatable columns for Rails 3 and 4, stored in the model table itself
+
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 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
--------------------------
I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in gems and real applications first, and only then cherry-picking the best-of-breed of most widely useful features for inclusion in the core.
-Thus we encourage everybody to experiment with new ideas and features in gems or other libraries and make them available to the community. (Don't forget to announce your work on our [mailing list](http://groups.google.com/group/rails-i18n)!)
+Thus we encourage everybody to experiment with new ideas and features in gems or other libraries and make them available to the community. (Don't forget to announce your work on our [mailing list](https://groups.google.com/forum/#!forum/rails-i18n)!)
If you find your own locale (language) missing from our [example translations data](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) repository for Ruby on Rails, please [_fork_](https://github.com/guides/fork-a-project-and-submit-your-modifications) the repository, add your data and send a [pull request](https://help.github.com/articles/about-pull-requests/).
diff --git a/guides/source/index.html.erb b/guides/source/index.html.erb
index 2fdf18a2e9..76f01fea0a 100644
--- a/guides/source/index.html.erb
+++ b/guides/source/index.html.erb
@@ -10,7 +10,9 @@ Ruby on Rails Guides
<div id="subCol">
<dl>
<dt></dt>
- <dd class="kindle">Rails Guides are also available for <%= link_to 'Kindle', @mobi %>.</dd>
+ <% unless @edge -%>
+ <dd class="kindle">Rails Guides are also available for <%= link_to 'Kindle', @mobi %>.</dd>
+ <% end -%>
<dd class="work-in-progress">Guides marked with this icon are currently being worked on and will not be available in the Guides Index menu. While still useful, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections.</dd>
</dl>
</div>
diff --git a/guides/source/initialization.md b/guides/source/initialization.md
index 1541ea38cd..d3b122c7fe 100644
--- a/guides/source/initialization.md
+++ b/guides/source/initialization.md
@@ -93,7 +93,7 @@ require 'bundler/setup' # Set up gems listed in the Gemfile.
In a standard Rails application, there's a `Gemfile` which declares all
dependencies of the application. `config/boot.rb` sets
-`ENV['BUNDLE_GEMFILE']` to the location of this file. If the Gemfile
+`ENV['BUNDLE_GEMFILE']` to the location of this file. If the `Gemfile`
exists, then `bundler/setup` is required. The require is used by Bundler to
configure the load path for your Gemfile's dependencies.
@@ -532,12 +532,12 @@ require "rails"
%w(
active_record/railtie
+ active_storage/engine
action_controller/railtie
action_view/railtie
action_mailer/railtie
active_job/railtie
action_cable/engine
- active_storage/engine
rails/test_unit/railtie
sprockets/railtie
).each do |railtie|
diff --git a/guides/source/kindle/rails_guides.opf.erb b/guides/source/kindle/rails_guides.opf.erb
index 63eeb007d7..1882ec1005 100644
--- a/guides/source/kindle/rails_guides.opf.erb
+++ b/guides/source/kindle/rails_guides.opf.erb
@@ -26,7 +26,7 @@
<item id="<%= document['url'] %>" media-type="text/html" href="<%= document['url'] %>" />
<% end %>
- <% %w{toc.html credits.html welcome.html copyright.html}.each do |url| %>
+ <% %w{toc.html welcome.html copyright.html}.each do |url| %>
<item id="<%= url %>" media-type="text/html" href="<%= url %>" />
<% end %>
@@ -38,7 +38,6 @@
<spine toc="toc">
<itemref idref="toc.html" />
<itemref idref="welcome.html" />
- <itemref idref="credits.html" />
<itemref idref="copyright.html" />
<% documents_flat.each do |document| %>
<itemref idref="<%= document['url'] %>" />
diff --git a/guides/source/kindle/toc.html.erb b/guides/source/kindle/toc.html.erb
index 0f4228ed6b..b77ac2e99d 100644
--- a/guides/source/kindle/toc.html.erb
+++ b/guides/source/kindle/toc.html.erb
@@ -18,7 +18,6 @@ Ruby on Rails Guides
<% end %>
<hr />
<ul>
- <li><a href="credits.html">Credits</a></li>
<li><a href="copyright.html">Copyright &amp; License</a></li>
</ul>
</div>
diff --git a/guides/source/kindle/toc.ncx.erb b/guides/source/kindle/toc.ncx.erb
index 5094fea4ca..9b73bc9bea 100644
--- a/guides/source/kindle/toc.ncx.erb
+++ b/guides/source/kindle/toc.ncx.erb
@@ -30,10 +30,6 @@
</navLabel>
<content src="welcome.html"/>
</navPoint>
- <navPoint class="article" id="credits" playOrder="3">
- <navLabel><text>Credits</text></navLabel>
- <content src="credits.html"/>
- </navPoint>
<navPoint class="article" id="copyright" playOrder="4">
<navLabel><text>Copyright &amp; License</text></navLabel>
<content src="copyright.html"/>
diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb
index 334595e4d2..d2024b726d 100644
--- a/guides/source/layout.html.erb
+++ b/guides/source/layout.html.erb
@@ -59,7 +59,6 @@
</div>
</li>
<li><a class="nav-item" href="contributing_to_ruby_on_rails.html">Contribute</a></li>
- <li><a class="nav-item" href="credits.html">Credits</a></li>
<li class="guides-index guides-index-small">
<select class="guides-index-item nav-item">
<option value="index.html">Guides Index</option>
@@ -99,9 +98,9 @@
To get started, you can read our <%= link_to 'documentation contributions', 'http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation' %> section.
</p>
<p>
- You may also find incomplete content, or stuff that is not up to date.
+ You may also find incomplete content or stuff that is not up to date.
Please do add any missing documentation for master. Make sure to check
- <%= link_to 'Edge Guides','http://edgeguides.rubyonrails.org' %> first to verify
+ <%= link_to 'Edge Guides', 'http://edgeguides.rubyonrails.org' %> first to verify
if the issues are already fixed or not on the master branch.
Check the <%= link_to 'Ruby on Rails Guides Guidelines', 'ruby_on_rails_guides_guidelines.html' %>
for style and conventions.
@@ -111,7 +110,7 @@
<%= link_to 'open an issue', 'https://github.com/rails/rails/issues' %>.
</p>
<p>And last but not least, any kind of discussion regarding Ruby on Rails
- documentation is very welcome in the <%= link_to 'rubyonrails-docs mailing list', 'https://groups.google.com/forum/#!forum/rubyonrails-docs' %>.
+ documentation is very welcome on the <%= link_to 'rubyonrails-docs mailing list', 'https://groups.google.com/forum/#!forum/rubyonrails-docs' %>.
</p>
</div>
</div>
@@ -124,15 +123,8 @@
</div>
</div>
- <script type="text/javascript" src="javascripts/jquery.min.js"></script>
- <script type="text/javascript" src="javascripts/responsive-tables.js"></script>
- <script type="text/javascript" src="javascripts/guides.js"></script>
<script type="text/javascript" src="javascripts/syntaxhighlighter.js"></script>
- <script type="text/javascript">
- syntaxhighlighterConfig = {
- autoLinks: false,
- };
- $(guidesIndex.bind);
- </script>
+ <script type="text/javascript" src="javascripts/guides.js"></script>
+ <script type="text/javascript" src="javascripts/responsive-tables.js"></script>
</body>
</html>
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index 76b325d0bf..15345c94b7 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -71,23 +71,25 @@ If we want to display the properties of all the books in our view, we can do so
<h1>Listing Books</h1>
<table>
- <tr>
- <th>Title</th>
- <th>Summary</th>
- <th></th>
- <th></th>
- <th></th>
- </tr>
-
-<% @books.each do |book| %>
- <tr>
- <td><%= book.title %></td>
- <td><%= book.content %></td>
- <td><%= link_to "Show", book %></td>
- <td><%= link_to "Edit", edit_book_path(book) %></td>
- <td><%= link_to "Remove", book, method: :delete, data: { confirm: "Are you sure?" } %></td>
- </tr>
-<% end %>
+ <thead>
+ <tr>
+ <th>Title</th>
+ <th>Content</th>
+ <th colspan="3"></th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <% @books.each do |book| %>
+ <tr>
+ <td><%= book.title %></td>
+ <td><%= book.content %></td>
+ <td><%= link_to "Show", book %></td>
+ <td><%= link_to "Edit", edit_book_path(book) %></td>
+ <td><%= link_to "Destroy", book, method: :delete, data: { confirm: "Are you sure?" } %></td>
+ </tr>
+ <% end %>
+ </tbody>
</table>
<br>
@@ -95,7 +97,7 @@ If we want to display the properties of all the books in our view, we can do so
<%= link_to "New book", new_book_path %>
```
-NOTE: The actual rendering is done by subclasses of `ActionView::TemplateHandlers`. This guide does not dig into that process, but it's important to know that the file extension on your view controls the choice of template handler. Beginning with Rails 2, the standard extensions are `.erb` for ERB (HTML with embedded Ruby), and `.builder` for Builder (XML generator).
+NOTE: The actual rendering is done by nested classes of the module [`ActionView::Template::Handlers`](http://api.rubyonrails.org/classes/ActionView/Template/Handlers.html). This guide does not dig into that process, but it's important to know that the file extension on your view controls the choice of template handler.
### Using `render`
@@ -230,14 +232,14 @@ You can send an HTML string back to the browser by using the `:html` option to
`render`:
```ruby
-render html: "<strong>Not Found</strong>".html_safe
+render html: helpers.tag.strong('Not Found')
```
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 composed with `html_safe`-aware APIs.
#### Rendering JSON
@@ -283,7 +285,7 @@ the response. Using `:plain` or `:html` might be more appropriate most of the
time.
NOTE: Unless overridden, your response returned from this render option will be
-`text/html`, as that is the default content type of Action Dispatch response.
+`text/plain`, as that is the default content type of Action Dispatch response.
#### Options for `render`
@@ -1264,7 +1266,7 @@ You can also pass in arbitrary local variables to any partial you are rendering
In this case, the partial will have access to a local variable `title` with the value "Products Page".
-TIP: Rails also makes a counter variable available within a partial called by the collection, named after the member of the collection followed by `_counter`. For example, if you're rendering `@products`, within the partial you can refer to `product_counter` to tell you how many times the partial has been rendered. This does not work in conjunction with the `as: :value` option.
+TIP: Rails also makes a counter variable available within a partial called by the collection, named after the title of the partial followed by `_counter`. For example, when rendering a collection `@products` the partial `_product.html.erb` can access the variable `product_counter` which indexes the number of times it has been rendered within the enclosing view.
You can also specify a second partial to be rendered between instances of the main partial by using the `:spacer_template` option:
diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md
index 1d6a4edb5b..2604d289e9 100644
--- a/guides/source/maintenance_policy.md
+++ b/guides/source/maintenance_policy.md
@@ -44,7 +44,7 @@ from.
In special situations, where someone from the Core Team agrees to support more series,
they are included in the list of supported series.
-**Currently included series:** `5.1.Z`.
+**Currently included series:** `5.2.Z`.
Security Issues
---------------
@@ -59,16 +59,16 @@ be built from 1.2.2, and then added to the end of 1-2-stable. This means that
security releases are easy to upgrade to if you're running the latest version
of Rails.
-**Currently included series:** `5.1.Z`, `5.0.Z`.
+**Currently included series:** `5.2.Z`, `5.1.Z`.
Severe Security Issues
----------------------
-For severe security issues we will provide new versions as above, and also the
+For severe security issues all releases in the current major series, and also the
last major release series will receive patches and new versions. The
classification of the security issue is judged by the core team.
-**Currently included series:** `5.1.Z`, `5.0.Z`, `4.2.Z`.
+**Currently included series:** `5.2.Z`, `5.1.Z`, `5.0.Z`, `4.2.Z`.
Unsupported Release Series
--------------------------
diff --git a/guides/source/plugins.md b/guides/source/plugins.md
index b3a7f544f5..15073af6be 100644
--- a/guides/source/plugins.md
+++ b/guides/source/plugins.md
@@ -359,7 +359,7 @@ When you run `bin/test`, you should see the tests all pass:
### Add an Instance Method
-This plugin will add a method named 'squawk' to any Active Record object that calls 'acts_as_yaffle'. The 'squawk'
+This plugin will add a method named 'squawk' to any Active Record object that calls `acts_as_yaffle`. The 'squawk'
method will simply set the value of one of the fields in the database.
To start out, write a failing test that shows the behavior you'd like:
@@ -392,7 +392,7 @@ end
```
Run the test to make sure the last two tests fail with an error that contains "NoMethodError: undefined method `squawk'",
-then update 'acts_as_yaffle.rb' to look like this:
+then update `acts_as_yaffle.rb` to look like this:
```ruby
# yaffle/lib/yaffle/acts_as_yaffle.rb
@@ -446,7 +446,7 @@ Publishing Your Gem
-------------------
Gem plugins currently in development can easily be shared from any Git repository. To share the Yaffle gem with others, simply
-commit the code to a Git repository (like GitHub) and add a line to the Gemfile of the application in question:
+commit the code to a Git repository (like GitHub) and add a line to the `Gemfile` of the application in question:
```ruby
gem "yaffle", git: "https://github.com/rails/yaffle.git"
diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md
index aa1476ecc0..1627205b7b 100644
--- a/guides/source/rails_on_rack.md
+++ b/guides/source/rails_on_rack.md
@@ -122,9 +122,11 @@ use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
+use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
+use Rack::TempfileReaper
run MyApp::Application.routes
```
@@ -249,7 +251,7 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
**`Rails::Rack::Logger`**
-* Notifies the logs that the request has began. After request is complete, flushes all the logs.
+* Notifies the logs that the request has begun. After the request is complete, flushes all the logs.
**`ActionDispatch::ShowExceptions`**
@@ -283,18 +285,26 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
* Sets up the flash keys. Only available if `config.action_controller.session_store` is set to a value.
+**`ActionDispatch::ContentSecurityPolicy::Middleware`**
+
+* Provides a DSL to configure a Content-Security-Policy header.
+
**`Rack::Head`**
* Converts HEAD requests to `GET` requests and serves them as so.
**`Rack::ConditionalGet`**
-* Adds support for "Conditional `GET`" so that server responds with nothing if page wasn't changed.
+* Adds support for "Conditional `GET`" so that server responds with nothing if the page wasn't changed.
**`Rack::ETag`**
* Adds ETag header on all String bodies. ETags are used to validate cache.
+**`Rack::TempfileReaper`**
+
+* Cleans up tempfiles used to buffer multipart requests.
+
TIP: It's possible to use any of the above middlewares in your custom Rack stack.
Resources
diff --git a/guides/source/routing.md b/guides/source/routing.md
index 638f77be13..7a83fee617 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -36,6 +36,8 @@ get '/patients/:id', to: 'patients#show'
the request is dispatched to the `patients` controller's `show` action with `{ id: '17' }` in `params`.
+NOTE: Rails uses snake_case for controller names here, if you have a multiple word controller like `MonsterTrucksController`, you want to use `monster_trucks#show` for example.
+
### Generating Paths and URLs from Code
You can also generate paths and URLs. If the route above is modified to be:
@@ -58,6 +60,26 @@ and this in the corresponding view:
then the router will generate the path `/patients/17`. This reduces the brittleness of your view and makes your code easier to understand. Note that the id does not need to be specified in the route helper.
+### Configuring the Rails Router
+
+The routes for your application or engine live in the file `config/routes.rb` and typically looks like this:
+
+```ruby
+Rails.application.routes.draw do
+ resources :brands, only: [:index, :show] do
+ resources :products, only: [:index, :show]
+ end
+
+ resource :basket, only: [:show, :update, :destroy]
+
+ resolve("Basket") { route_for(:basket) }
+end
+```
+
+Since this is a regular Ruby source file you can use all of its features to help you define your routes but be careful with variable names as they can clash with the DSL methods of the router.
+
+NOTE: The `Rails.application.routes.draw do ... end` block that wraps your route definitions is required to establish the scope for the router DSL and must not be deleted.
+
Resource Routing: the Rails Default
-----------------------------------
@@ -549,7 +571,7 @@ In particular, simple routing makes it very easy to map legacy URLs to new Rails
When you set up a regular route, you supply a series of symbols that Rails maps to parts of an incoming HTTP request. For example, consider this route:
```ruby
-get 'photos(/:id)', to: :display
+get 'photos(/:id)', to: 'photos#display'
```
If an incoming request of `/photos/1` is processed by this route (because it hasn't matched any previous route in the file), then the result will be to invoke the `display` action of the `PhotosController`, and to make the final parameter `"1"` available as `params[:id]`. This route will also route the incoming request of `/photos` to `PhotosController#display`, since `:id` is an optional parameter, denoted by parentheses.
@@ -852,6 +874,49 @@ You can specify unicode character routes directly. For example:
get 'こんにちは', to: 'welcome#index'
```
+### Direct routes
+
+You can create custom URL helpers directly. For example:
+
+```ruby
+direct :homepage do
+ "http://www.rubyonrails.org"
+end
+
+# >> homepage_url
+# => "http://www.rubyonrails.org"
+```
+
+The return value of the block must be a valid argument for the `url_for` method. So, you can pass a valid string URL, Hash, Array, an Active Model instance, or an Active Model class.
+
+```ruby
+direct :commentable do |model|
+ [ model, anchor: model.dom_id ]
+end
+
+direct :main do
+ { controller: 'pages', action: 'index', subdomain: 'www' }
+end
+```
+
+### Using `resolve`
+
+The `resolve` method allows customizing polymorphic mapping of models. For example:
+
+``` ruby
+resource :basket
+
+resolve("Basket") { [:basket] }
+```
+
+``` erb
+<%= form_for @basket do |form| %>
+ <!-- basket form -->
+<% end %>
+```
+
+This will generate the singular URL `/basket` instead of the usual `/baskets/:id`.
+
Customizing Resourceful Routes
------------------------------
@@ -1140,7 +1205,7 @@ $ bin/rails routes -c Comments
$ bin/rails routes -c Articles::CommentsController
```
-TIP: You'll find that the output from `rails routes` is much more readable if you widen your terminal window until the output lines don't wrap.
+TIP: You'll find that the output from `rails routes` is much more readable if you widen your terminal window until the output lines don't wrap. You can also use --expanded option to turn on the expanded table formatting mode.
### Testing Routes
diff --git a/guides/source/security.md b/guides/source/security.md
index 746da5e634..3ac50fb147 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -1,7 +1,7 @@
**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
-Ruby on Rails Security Guide
-============================
+Securing Rails Applications
+===========================
This manual describes common security problems in web applications and how to avoid them with Rails.
@@ -52,7 +52,7 @@ User.find(session[:user_id])
NOTE: _The session ID is a 32-character random hex string._
-The session ID is generated using `SecureRandom.hex` which generates a random hex string using platform specific methods (such as OpenSSL, /dev/urandom or Win32) for generating cryptographically secure random numbers. Currently it is not feasible to brute-force Rails' session IDs.
+The session ID is generated using `SecureRandom.hex` which generates a random hex string using platform specific methods (such as OpenSSL, /dev/urandom or Win32 CryptoAPI) for generating cryptographically secure random numbers. Currently it is not feasible to brute-force Rails' session IDs.
### Session Hijacking
@@ -74,7 +74,7 @@ Hence, the cookie serves as temporary authentication for the web application. An
* Instead of stealing a cookie unknown to the attacker, they fix a user's session identifier (in the cookie) known to them. Read more about this so-called session fixation later.
-The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from $10-$1000 (depending on the available amount of funds), $0.40-$20 for credit card numbers, $1-$8 for online auction site accounts and $4-$30 for email passwords, according to the [Symantec Global Internet Security Threat Report](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf).
+The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from 0.5%-10% of account balance, $0.5-$30 for credit card numbers ($20-$60 with full details), $0.1-$1.5 for identities (Name, SSN & DOB), $20-$50 for retailer accounts, and $6-$10 for cloud service provider accounts, according to the [Symantec Internet Security Threat Report (2017)](https://www.symantec.com/content/dam/symantec/docs/reports/istr-22-2017-en.pdf).
### Session Guidelines
@@ -85,37 +85,117 @@ This will also be a good idea, if you modify the structure of an object and old
* _Critical data should not be stored in session_. If the user clears their cookies or closes the browser, they will be lost. And with a client-side session storage, the user can read the data.
-### Session Storage
+### Encrypted Session Storage
NOTE: _Rails provides several storage mechanisms for the session hashes. The most important is `ActionDispatch::Session::CookieStore`._
-Rails 2 introduced a new default session storage, CookieStore. CookieStore saves the session hash directly in a cookie on the client-side. The server retrieves the session hash from the cookie and eliminates the need for a session ID. That will greatly increase the speed of the application, but it is a controversial storage option and you have to think about the security implications of it:
+The `CookieStore` saves the session hash directly in a cookie on the
+client-side. The server retrieves the session hash from the cookie and
+eliminates the need for a session ID. That will greatly increase the
+speed of the application, but it is a controversial storage option and
+you have to think about the security implications and storage
+limitations of it:
+
+* Cookies imply a strict size limit of 4kB. This is fine as you should
+ not store large amounts of data in a session anyway, as described
+ before. Storing the current user's database id in a session is common
+ practice.
+
+* Session cookies do not invalidate themselves and can be maliciously
+ reused. It may be a good idea to have your application invalidate old
+ session cookies using a stored timestamp.
+
+The `CookieStore` uses the
+[encrypted](http://api.rubyonrails.org/classes/ActionDispatch/Cookies/ChainedCookieJars.html#method-i-encrypted)
+cookie jar to provide a secure, encrypted location to store session
+data. Cookie-based sessions thus provide both integrity as well as
+confidentiality to their contents. The encryption key, as well as the
+verification key used for
+[signed](http://api.rubyonrails.org/classes/ActionDispatch/Cookies/ChainedCookieJars.html#method-i-signed)
+cookies, is derived from the `secret_key_base` configuration value.
+
+As of Rails 5.2 encrypted cookies and sessions are protected using AES
+GCM encryption. This form of encryption is a type of Authenticated
+Encryption and couples authentication and encryption in single step
+while also producing shorter ciphertexts as compared to other
+algorithms previously used. The key for cookies encrypted with AES GCM
+are derived using a salt value defined by the
+`config.action_dispatch.authenticated_encrypted_cookie_salt`
+configuration value.
+
+Prior to this version, encrypted cookies were secured using AES in CBC
+mode with HMAC using SHA1 for authentication. The keys for this type of
+encryption and for HMAC verification were derived via the salts defined
+by `config.action_dispatch.encrypted_cookie_salt` and
+`config.action_dispatch.encrypted_signed_cookie_salt` respectively.
+
+Prior to Rails version 4 in both versions 2 and 3, session cookies were
+protected using only HMAC verification. As such, these session cookies
+only provided integrity to their content because the actual session data
+was stored in plaintext encoded as base64. This is how `signed` cookies
+work in the current version of Rails. These kinds of cookies are still
+useful for protecting the integrity of certain client-stored data and
+information.
+
+__Do not use a trivial secret for the `secret_key_base`, i.e. a word
+from a dictionary, or one which is shorter than 30 characters! Instead
+use `rails secret` to generate secret keys!__
+
+It is also important to use different salt values for encrypted and
+signed cookies. Using the same value for different salt configuration
+values may lead to the same derived key being used for different
+security features which in turn may weaken the strength of the key.
-* Cookies imply a strict size limit of 4kB. This is fine as you should not store large amounts of data in a session anyway, as described before. _Storing the current user's database id in a session is usually ok_.
+In test and development applications get a `secret_key_base` derived from the app name. Other environments must use a random key present in `config/credentials.yml.enc`, shown here in its decrypted state:
-* The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret (`secrets.secret_token`) and inserted into the end of the cookie.
+ secret_key_base: 492f...
-In Rails 4, encrypted cookies through AES in CBC mode with HMAC using SHA1 for
-verification was introduced. This prevents the user from accessing and tampering
-the content of the cookie. Thus the session becomes a more secure place to store
-data. The encryption is performed using a server-side `secret_key_base`.
-Two salts are used when deriving keys for encryption and verification. These
-salts are set via the `config.action_dispatch.encrypted_cookie_salt` and
-`config.action_dispatch.encrypted_signed_cookie_salt` configuration values.
+If you have received an application where the secret was exposed (e.g. an application whose source was shared), strongly consider changing the secret.
-Rails 5.2 uses AES-GCM for the encryption which couples authentication
-and encryption in one faster step and produces shorter ciphertexts.
+### Rotating Encrypted and Signed Cookies Configurations
-Encrypted cookies are automatically upgraded if the
-`config.action_dispatch.use_authenticated_cookie_encryption` is enabled.
+Rotation is ideal for changing cookie configurations and ensuring old cookies
+aren't immediately invalid. Your users then have a chance to visit your site,
+get their cookie read with an old configuration and have it rewritten with the
+new change. The rotation can then be removed once you're comfortable enough
+users have had their chance to get their cookies upgraded.
-_Do not use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters! Instead use `rails secret` to generate secret keys!_
+It's possible to rotate the ciphers and digests used for encrypted and signed cookies.
-In test and development applications get a `secret_key_base` derived from the app name. Other environments must use a random key present in `config/credentials.yml.enc`, shown here in its decrypted state:
+For instance to change the digest used for signed cookies from SHA1 to SHA256,
+you would first assign the new configuration value:
- secret_key_base: 492f...
+```ruby
+Rails.application.config.action_dispatch.signed_cookie_digest = "SHA256"
+```
-If you have received an application where the secret was exposed (e.g. an application whose source was shared), strongly consider changing the secret.
+Now add a rotation for the old SHA1 digest so existing cookies are
+seamlessly upgraded to the new SHA256 digest.
+
+```ruby
+Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
+ cookies.rotate :signed, digest: "SHA1"
+end
+```
+
+Then any written signed cookies will be digested with SHA256. Old cookies
+that were written with SHA1 can still be read, and if accessed will be written
+with the new digest so they're upgraded and won't be invalid when you remove the
+rotation.
+
+Once users with SHA1 digested signed cookies should no longer have a chance to
+have their cookies rewritten, remove the rotation.
+
+While you can setup as many rotations as you'd like it's not common to have many
+rotations going at any one time.
+
+For more details on key rotation with encrypted and signed messages as
+well as the various options the `rotate` method accepts, please refer to
+the
+[MessageEncryptor API](http://api.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html)
+and
+[MessageVerifier API](http://api.rubyonrails.org/classes/ActiveSupport/MessageVerifier.html)
+documentation.
### Replay Attacks for CookieStore Sessions
@@ -137,7 +217,7 @@ The best _solution against it is not to store this kind of data in a session, bu
NOTE: _Apart from stealing a user's session ID, the attacker may fix a session ID known to them. This is called session fixation._
-![Session fixation](images/session_fixation.png)
+![Session fixation](images/security/session_fixation.png)
This attack focuses on fixing a user's session ID known to the attacker, and forcing the user's browser into using this ID. It is therefore not necessary for the attacker to steal the session ID afterwards. Here is how this attack works:
@@ -192,7 +272,7 @@ Cross-Site Request Forgery (CSRF)
This attack method works by including malicious code or a link in a page that accesses a web application that the user is believed to have authenticated. If the session for that web application has not timed out, an attacker may execute unauthorized commands.
-![](images/csrf.png)
+![](images/security/csrf.png)
In the [session chapter](#sessions) you have learned that most Rails applications use cookie-based sessions. Either they store the session ID in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is that if the request comes from a site of a different domain, it will also send the cookie. Let's start with an example:
@@ -245,7 +325,7 @@ Or the attacker places the code into the onmouseover event handler of an image:
There are many other possibilities, like using a `<script>` tag to make a cross-site request to a URL with a JSONP or JavaScript response. The response is executable code that the attacker can find a way to run, possibly extracting sensitive data. To protect against this data leakage, we must disallow cross-site `<script>` tags. Ajax requests, however, obey the browser's same-origin policy (only your own site is allowed to initiate `XmlHttpRequest`) so we can safely allow them to return JavaScript responses.
-Note: We can't distinguish a `<script>` tag's origin—whether it's a tag on your own site or on some other malicious site—so we must block all `<script>` across the board, even if it's actually a safe same-origin script served from your own site. In these cases, explicitly skip CSRF protection on actions that serve JavaScript meant for a `<script>` tag.
+NOTE: We can't distinguish a `<script>` tag's origin—whether it's a tag on your own site or on some other malicious site—so we must block all `<script>` across the board, even if it's actually a safe same-origin script served from your own site. In these cases, explicitly skip CSRF protection on actions that serve JavaScript meant for a `<script>` tag.
To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created Rails applications:
@@ -394,7 +474,7 @@ The common admin interface works like this: it's located at www.example.com/admi
* Does the admin really have to access the interface from everywhere in the world? Think about _limiting the login to a bunch of source IP addresses_. Examine request.remote_ip to find out about the user's IP address. This is not bullet-proof, but a great barrier. Remember that there might be a proxy in use, though.
-* _Put the admin interface to a special sub-domain_ such as admin.application.com and make it a separate application with its own user management. This makes stealing an admin cookie from the usual domain, www.application.com, impossible. This is because of the same origin policy in your browser: An injected (XSS) script on www.application.com may not read the cookie for admin.application.com and vice-versa.
+* _Put the admin interface to a special subdomain_ such as admin.application.com and make it a separate application with its own user management. This makes stealing an admin cookie from the usual domain, www.application.com, impossible. This is because of the same origin policy in your browser: An injected (XSS) script on www.application.com may not read the cookie for admin.application.com and vice-versa.
User Management
---------------
@@ -456,7 +536,7 @@ Depending on your web application, there may be more ways to hijack the user's a
INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect registration forms from attackers and comment forms from automatic spam bots by asking the user to type the letters of a distorted image. This is the positive CAPTCHA, but there is also the negative CAPTCHA. The idea of a negative CAPTCHA is not for a user to prove that they are human, but reveal that a robot is a robot._
-A popular positive CAPTCHA API is [reCAPTCHA](http://recaptcha.net/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API.
+A popular positive CAPTCHA API is [reCAPTCHA](https://developers.google.com/recaptcha/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API.
You will get two keys from the API, a public and a private key, which you have to put into your Rails environment. After that you can use the recaptcha_tags method in the view, and the verify_recaptcha method in the controller. Verify_recaptcha will return false if the validation fails.
The problem with CAPTCHAs is that they have a negative impact on the user experience. Additionally, some visually impaired users have found certain kinds of distorted CAPTCHAs difficult to read. Still, positive CAPTCHAs are one of the best methods to prevent all kinds of bots from submitting forms.
@@ -471,7 +551,7 @@ Here are some ideas how to hide honeypot fields by JavaScript and/or CSS:
* make the elements very small or color them the same as the background of the page
* leave the fields displayed, but tell humans to leave them blank
-The most simple negative CAPTCHA is one hidden honeypot field. On the server side, you will check the value of the field: If it contains any text, it must be a bot. Then, you can either ignore the post or return a positive result, but not saving the post to the database. This way the bot will be satisfied and moves on. You can do this with annoying users, too.
+The most simple negative CAPTCHA is one hidden honeypot field. On the server side, you will check the value of the field: If it contains any text, it must be a bot. Then, you can either ignore the post or return a positive result, but not saving the post to the database. This way the bot will be satisfied and moves on.
You can find more sophisticated negative CAPTCHAs in Ned Batchelder's [blog post](http://nedbatchelder.com/text/stopbots.html):
@@ -493,18 +573,6 @@ config.filter_parameters << :password
NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`.
-### Good Passwords
-
-INFO: _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._
-
-Bruce Schneier, a security technologist, [has analyzed](http://www.schneier.com/blog/archives/2006/12/realworld_passw.html) 34,000 real-world user names and passwords from the MySpace phishing attack mentioned [below](#examples-from-the-underground). It turns out that most of the passwords are quite easy to crack. The 20 most common passwords are:
-
-password1, abc123, myspace1, password, blink182, qwerty1, ****you, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, and monkey.
-
-It is interesting that only 4% of these passwords were dictionary words and the great majority is actually alphanumeric. However, password cracker dictionaries contain a large number of today's passwords, and they try out all kinds of (alphanumerical) combinations. If an attacker knows your user name and you use a weak password, your account will be easily cracked.
-
-A good password is a long alphanumeric combination of mixed cases. As this is quite hard to remember, it is advisable to enter only the _first letters of a sentence that you can easily remember_. For example "The quick brown fox jumps over the lazy dog" will be "Tqbfjotld". Note that this is just an example, you should not use well known phrases like these, as they might appear in cracker dictionaries, too.
-
### Regular Expressions
INFO: _A common pitfall in Ruby's regular expressions is to match the string's beginning and end by ^ and $, instead of \A and \z._
@@ -794,7 +862,7 @@ In December 2006, 34,000 actual user names and passwords were stolen in a [MySpa
INFO: _CSS Injection is actually JavaScript injection, because some browsers (IE, some versions of Safari and others) allow JavaScript in CSS. Think twice about allowing custom CSS in your web application._
-CSS Injection is explained best by the well-known [MySpace Samy worm](https://samy.pl/popular/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm.
+CSS Injection is explained best by the well-known [MySpace Samy worm](https://samy.pl/myspace/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm.
MySpace blocked many tags, but allowed CSS. So the worm's author put JavaScript into CSS like this:
@@ -990,7 +1058,10 @@ Every HTTP response from your Rails application receives the following default s
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
- 'X-Content-Type-Options' => 'nosniff'
+ 'X-Content-Type-Options' => 'nosniff',
+ 'X-Download-Options' => 'noopen',
+ 'X-Permitted-Cross-Domain-Policies' => 'none',
+ 'Referrer-Policy' => 'strict-origin-when-cross-origin'
}
```
@@ -1018,6 +1089,118 @@ Here is a list of common headers:
* **Access-Control-Allow-Origin:** Used to control which sites are allowed to bypass same origin policies and send cross-origin requests.
* **Strict-Transport-Security:** [Used to control if the browser is allowed to only access a site over a secure connection](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security)
+### Content Security Policy
+
+Rails provides a DSL that allows you to configure a
+[Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)
+for your application. You can configure a global default policy and then
+override it on a per-resource basis and even use lambdas to inject per-request
+values into the header such as account subdomains in a multi-tenant application.
+
+Example global policy:
+
+```ruby
+# config/initializers/content_security_policy.rb
+Rails.application.config.content_security_policy do |policy|
+ policy.default_src :self, :https
+ policy.font_src :self, :https, :data
+ policy.img_src :self, :https, :data
+ policy.object_src :none
+ policy.script_src :self, :https
+ policy.style_src :self, :https
+
+ # Specify URI for violation reports
+ policy.report_uri "/csp-violation-report-endpoint"
+end
+```
+
+Example controller overrides:
+
+```ruby
+# Override policy inline
+class PostsController < ApplicationController
+ content_security_policy do |p|
+ p.upgrade_insecure_requests true
+ end
+end
+
+# Using literal values
+class PostsController < ApplicationController
+ content_security_policy do |p|
+ p.base_uri "https://www.example.com"
+ end
+end
+
+# Using mixed static and dynamic values
+class PostsController < ApplicationController
+ content_security_policy do |p|
+ p.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
+ end
+end
+
+# Disabling the global CSP
+class LegacyPagesController < ApplicationController
+ content_security_policy false, only: :index
+end
+```
+
+Use the `content_security_policy_report_only`
+configuration attribute to set
+[Content-Security-Policy-Report-Only](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only)
+in order to report only content violations for migrating
+legacy content
+
+```ruby
+# config/initializers/content_security_policy.rb
+Rails.application.config.content_security_policy_report_only = true
+```
+
+```ruby
+# Controller override
+class PostsController < ApplicationController
+ content_security_policy_report_only only: :index
+end
+```
+
+You can enable automatic nonce generation:
+
+```ruby
+# config/initializers/content_security_policy.rb
+Rails.application.config.content_security_policy do |policy|
+ policy.script_src :self, :https
+end
+
+Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
+```
+
+Then you can add an automatic nonce value by passing `nonce: true`
+as part of `html_options`. Example:
+
+```html+erb
+<%= javascript_tag nonce: true do -%>
+ alert('Hello, World!');
+<% end -%>
+```
+
+The same works with `javascript_include_tag`:
+
+```html+erb
+<%= javascript_include_tag "script", nonce: true %>
+```
+
+Use [`csp_meta_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/CspHelper.html#method-i-csp_meta_tag)
+helper to create a meta tag "csp-nonce" with the per-session nonce value
+for allowing inline `<script>` tags.
+
+```html+erb
+<head>
+ <%= csp_meta_tag %>
+</head>
+```
+
+This is used by the Rails UJS helper to create dynamically
+loaded inline `<script>` elements.
+
Environmental Security
----------------------
@@ -1049,7 +1232,7 @@ If you want an exception to be raised when some key is blank, use the bang
version:
```ruby
-Rails.application.credentials.some_api_key! # => raises KeyError: key not found: :some_api_key
+Rails.application.credentials.some_api_key! # => raises KeyError: :some_api_key is blank
```
Additional Resources
diff --git a/guides/source/testing.md b/guides/source/testing.md
index 4ee3267261..9fd29672c7 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -1,7 +1,7 @@
**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
-A Guide to Testing Rails Applications
-=====================================
+Testing Rails Applications
+==========================
This guide covers built-in mechanisms in Rails for testing your application.
@@ -33,8 +33,8 @@ Rails creates a `test` directory for you as soon as you create a Rails project u
```bash
$ ls -F test
-controllers/ helpers/ mailers/ system/ test_helper.rb
-fixtures/ integration/ models/ application_system_test_case.rb
+application_system_test_case.rb fixtures/ integration/ models/ test_helper.rb
+controllers/ helpers/ mailers/ system/
```
The `helpers`, `mailers`, and `models` directories are meant to hold tests for view helpers, mailers, and models, respectively. The `controllers` directory is meant to hold tests for controllers, routes, and views. The `integration` directory is meant to hold tests for interactions between controllers.
@@ -319,6 +319,8 @@ specify to make your test failure messages clearer.
| `assert_not_includes( collection, obj, [msg] )` | Ensures that `obj` is not in `collection`.|
| `assert_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.|
| `assert_not_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.|
+| `assert_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` have a relative error less than `epsilon`.|
+| `assert_not_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` don't have a relative error less than `epsilon`.|
| `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_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.|
@@ -431,16 +433,8 @@ at the end of test run and so on. Check the documentation of the test runner as
```bash
$ bin/rails test -h
-minitest options:
- -h, --help Display this help.
- -s, --seed SEED Sets random seed. Also via env. Eg: SEED=n rake
- -v, --verbose Verbose. Show progress processing files.
- -n, --name PATTERN Filter run on /regexp/ or string.
- --exclude PATTERN Exclude /regexp/ or string from run.
-
-Known extensions: rails, pride
-
Usage: bin/rails test [options] [files or directories]
+
You can run a single test by appending a line number to a filename:
bin/rails test test/models/user_test.rb:27
@@ -451,13 +445,105 @@ You can run multiple files and directories at the same time:
By default test failures and errors are reported inline during a run.
-Rails options:
+minitest options:
+ -h, --help Display this help.
+ --no-plugins Bypass minitest plugin auto-loading (or set $MT_NO_PLUGINS).
+ -s, --seed SEED Sets random seed. Also via env. Eg: SEED=n rake
+ -v, --verbose Verbose. Show progress processing files.
+ -n, --name PATTERN Filter run on /regexp/ or string.
+ --exclude PATTERN Exclude /regexp/ or string from run.
+
+Known extensions: rails, pride
-w, --warnings Run with Ruby warnings enabled
- -e, --environment Run tests in the ENV environment
+ -e, --environment ENV Run tests in the ENV environment
-b, --backtrace Show the complete backtrace
-d, --defer-output Output test failures and errors after the test run
-f, --fail-fast Abort test run on first failure or error
-c, --[no-]color Enable color in the output
+ -p, --pride Pride. Show your testing pride!
+```
+
+Parallel Testing
+----------------
+
+Parallel testing allows you to parallelize your test suite. While forking processes is the
+default method, threading is supported as well. Running tests in parallel reduces the time it
+takes your entire test suite to run.
+
+### Parallel testing with processes
+
+The default parallelization method is to fork processes using Ruby's DRb system. The processes
+are forked based on the number of workers provided. The default is 2, but can be changed by the
+number passed to the parallelize method. Active Record automatically handles creating and
+migrating a new database for each worker to use.
+
+To enable parallelization add the following to your `test_helper.rb`:
+
+```
+class ActiveSupport::TestCase
+ parallelize(workers: 2)
+end
+```
+
+The number of workers passed is the number of times the process will be forked. You may want to
+parallelize your local test suite differently from your CI, so an environment variable is provided
+to be able to easily change the number of workers a test run should use:
+
+```
+PARALLEL_WORKERS=15 bin/rails test
+```
+
+When parallelizing tests, Active Record automatically handles creating and migrating a database for each
+process. The databases will be suffixed with the number corresponding to the worker. For example, if you
+have 2 workers the tests will create `test-database-0` and `test-database-1` respectively.
+
+If the number of workers passed is 1 or fewer the processes will not be forked and the tests will not
+be parallelized and the tests will use the original `test-database` database.
+
+Two hooks are provided, one runs when the process is forked, and one runs before the processes are closed.
+These can be useful if your app uses multiple databases or perform other tasks that depend on the number of
+workers.
+
+The `parallelize_setup` method is called right after the processes are forked. The `parallelize_teardown` metod
+is called right before the processes are closed.
+
+```
+class ActiveSupport::TestCase
+ parallelize_setup do |worker|
+ # setup databases
+ end
+
+ parallelize_teardown do |worker|
+ # cleanup database
+ end
+
+ parallelize(workers: 2)
+end
+```
+
+These methods are not needed or available when using parallel testing with threads.
+
+### Parallel testing with threads
+
+If you prefer using threads or are using JRuby, a threaded parallelization option is provided. The threaded
+parallelizer is backed by Minitest's `Parallel::Executor`.
+
+To change the parallelization method to use threads over forks put the following in your `test_helper.rb`
+
+```
+class ActiveSupport::TestCase
+ parallelize(workers: 2, with: :threads)
+end
+```
+
+Rails applications generated from JRuby will automatically include the `with: :threads` option.
+
+The number of workers passed to `parallelize` determines the number of threads the tests will use. You may
+want to parallelize your local test suite differently from your CI, so an environment variable is provided
+to be able to easily change the number of workers a test run should use:
+
+```
+PARALLEL_WORKERS=15 bin/rails test
```
The Test Database
@@ -605,7 +691,7 @@ System Testing
--------------
System tests allow you to test user interactions with your application, running tests
-in either a real or a headless browser. System tests uses Capybara under the hood.
+in either a real or a headless browser. System tests use Capybara under the hood.
For creating Rails system tests, you use the `test/system` directory in your
application. Rails provides a generator to create a system test skeleton for you.
@@ -645,7 +731,7 @@ system tests should live.
If you want to change the default settings you can change what the system
tests are "driven by". Say you want to change the driver from Selenium to
-Poltergeist. First add the `poltergeist` gem to your Gemfile. Then in your
+Poltergeist. First add the `poltergeist` gem to your `Gemfile`. Then in your
`application_system_test_case.rb` file do the following:
```ruby
@@ -671,6 +757,17 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
end
```
+If you want to use a headless browser, you could use Headless Chrome or Headless Firefox by adding
+`headless_chrome` or `headless_firefox` in the `:using` argument.
+
+```ruby
+require "test_helper"
+
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :headless_chrome
+end
+```
+
If your Capybara configuration requires more setup than provided by Rails, this
additional configuration could be added into the `application_system_test_case.rb`
file.
@@ -765,9 +862,37 @@ Then the test will fill in the title and body of the article with the specified
text. Once the fields are filled in, "Create Article" is clicked on which will
send a POST request to create the new article in the database.
-We will be redirected back to the the articles index page and there we assert
+We will be redirected back to the articles index page and there we assert
that the text from the new article's title is on the articles index page.
+#### Testing for multiple screen sizes
+If you want to test for mobile sizes on top of testing for desktop,
+you can create another class that inherits from SystemTestCase and use in your
+test suite. In this example a file called `mobile_system_test_case.rb` is created
+in the `/test` directory with the following configuration.
+
+```ruby
+require "test_helper"
+
+class MobileSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :chrome, screen_size: [375, 667]
+end
+```
+To use this configuration, create a test inside `test/system` that inherits from `MobileSystemTestCase`.
+Now you can test your app using multiple different configurations.
+
+```ruby
+require "mobile_system_test_case"
+
+class PostsTest < MobileSystemTestCase
+
+ test "visiting the index" do
+ visit posts_url
+ assert_selector "h1", text: "Posts"
+ end
+end
+```
+
#### Taking it further
The beauty of system testing is that it is similar to integration testing in
@@ -1360,7 +1485,7 @@ located under the `test/helpers` directory.
Given we have the following helper:
```ruby
-module UserHelper
+module UsersHelper
def link_to_user(user)
link_to "#{user.first_name} #{user.last_name}", user
end
@@ -1370,7 +1495,7 @@ end
We can test the output of this method like this:
```ruby
-class UserHelperTest < ActionView::TestCase
+class UsersHelperTest < ActionView::TestCase
test "should return the user's full name" do
user = users(:david)
@@ -1476,7 +1601,7 @@ Functional testing for mailers involves more than just checking that the email b
```ruby
require 'test_helper'
-class UserControllerTest < ActionDispatch::IntegrationTest
+class UsersControllerTest < ActionDispatch::IntegrationTest
test "invite friend" do
assert_difference 'ActionMailer::Base.deliveries.size', +1 do
post invite_friend_url, params: { email: 'friend@example.com' }
@@ -1494,7 +1619,7 @@ Testing Jobs
------------
Since your custom jobs can be queued at different levels inside your application,
-you'll need to test both, the jobs themselves (their behavior when they get enqueued)
+you'll need to test both the jobs themselves (their behavior when they get enqueued)
and that other entities correctly enqueue them.
### A Basic Test Case
@@ -1513,7 +1638,7 @@ class BillingJobTest < ActiveJob::TestCase
end
```
-This test is pretty simple and only asserts that the job get the work done
+This test is pretty simple and only asserts that the job got the work done
as expected.
By default, `ActiveJob::TestCase` will set the queue adapter to `:test` so that
diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md
new file mode 100644
index 0000000000..e4febc7507
--- /dev/null
+++ b/guides/source/threading_and_code_execution.md
@@ -0,0 +1,324 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+
+Threading and Code Execution in Rails
+=====================================
+
+After reading this guide, you will know:
+
+* What code Rails will automatically execute concurrently
+* How to integrate manual concurrency with Rails internals
+* How to wrap all application code
+* How to affect application reloading
+
+--------------------------------------------------------------------------------
+
+Automatic Concurrency
+---------------------
+
+Rails automatically allows various operations to be performed at the same time.
+
+When using a threaded web server, such as the default Puma, multiple HTTP
+requests will be served simultaneously, with each request provided its own
+controller instance.
+
+Threaded Active Job adapters, including the built-in Async, will likewise
+execute several jobs at the same time. Action Cable channels are managed this
+way too.
+
+These mechanisms all involve multiple threads, each managing work for a unique
+instance of some object (controller, job, channel), while sharing the global
+process space (such as classes and their configurations, and global variables).
+As long as your code doesn't modify any of those shared things, it can mostly
+ignore that other threads exist.
+
+The rest of this guide describes the mechanisms Rails uses to make it "mostly
+ignorable", and how extensions and applications with special needs can use them.
+
+Executor
+--------
+
+The Rails Executor separates application code from framework code: any time the
+framework invokes code you've written in your application, it will be wrapped by
+the Executor.
+
+The Executor consists of two callbacks: `to_run` and `to_complete`. The Run
+callback is called before the application code, and the Complete callback is
+called after.
+
+### Default callbacks
+
+In a default Rails application, the Executor callbacks are used to:
+
+* track which threads are in safe positions for autoloading and reloading
+* enable and disable the Active Record query cache
+* return acquired Active Record connections to the pool
+* constrain internal cache lifetimes
+
+Prior to Rails 5.0, some of these were handled by separate Rack middleware
+classes (such as `ActiveRecord::ConnectionAdapters::ConnectionManagement`), or
+directly wrapping code with methods like
+`ActiveRecord::Base.connection_pool.with_connection`. The Executor replaces
+these with a single more abstract interface.
+
+### Wrapping application code
+
+If you're writing a library or component that will invoke application code, you
+should wrap it with a call to the executor:
+
+```ruby
+Rails.application.executor.wrap do
+ # call application code here
+end
+```
+
+TIP: If you repeatedly invoke application code from a long-running process, you
+may want to wrap using the Reloader instead.
+
+Each thread should be wrapped before it runs application code, so if your
+application manually delegates work to other threads, such as via `Thread.new`
+or Concurrent Ruby features that use thread pools, you should immediately wrap
+the block:
+
+```ruby
+Thread.new do
+ Rails.application.executor.wrap do
+ # your code here
+ end
+end
+```
+
+NOTE: Concurrent Ruby uses a `ThreadPoolExecutor`, which it sometimes configures
+with an `executor` option. Despite the name, it is unrelated.
+
+The Executor is safely re-entrant; if it is already active on the current
+thread, `wrap` is a no-op.
+
+If it's impractical to wrap the application code in a block (for
+example, the Rack API makes this problematic), you can also use the `run!` /
+`complete!` pair:
+
+```ruby
+Thread.new do
+ execution_context = Rails.application.executor.run!
+ # your code here
+ensure
+ execution_context.complete! if execution_context
+end
+```
+
+### Concurrency
+
+The Executor will put the current thread into `running` mode in the Load
+Interlock. This operation will block temporarily if another thread is currently
+either autoloading a constant or unloading/reloading the application.
+
+Reloader
+--------
+
+Like the Executor, the Reloader also wraps application code. If the Executor is
+not already active on the current thread, the Reloader will invoke it for you,
+so you only need to call one. This also guarantees that everything the Reloader
+does, including all its callback invocations, occurs wrapped inside the
+Executor.
+
+```ruby
+Rails.application.reloader.wrap do
+ # call application code here
+end
+```
+
+The Reloader is only suitable where a long-running framework-level process
+repeatedly calls into application code, such as for a web server or job queue.
+Rails automatically wraps web requests and Active Job workers, so you'll rarely
+need to invoke the Reloader for yourself. Always consider whether the Executor
+is a better fit for your use case.
+
+### Callbacks
+
+Before entering the wrapped block, the Reloader will check whether the running
+application needs to be reloaded -- for example, because a model's source file has
+been modified. If it determines a reload is required, it will wait until it's
+safe, and then do so, before continuing. When the application is configured to
+always reload regardless of whether any changes are detected, the reload is
+instead performed at the end of the block.
+
+The Reloader also provides `to_run` and `to_complete` callbacks; they are
+invoked at the same points as those of the Executor, but only when the current
+execution has initiated an application reload. When no reload is deemed
+necessary, the Reloader will invoke the wrapped block with no other callbacks.
+
+### Class Unload
+
+The most significant part of the reloading process is the Class Unload, where
+all autoloaded classes are removed, ready to be loaded again. This will occur
+immediately before either the Run or Complete callback, depending on the
+`reload_classes_only_on_change` setting.
+
+Often, additional reloading actions need to be performed either just before or
+just after the Class Unload, so the Reloader also provides `before_class_unload`
+and `after_class_unload` callbacks.
+
+### Concurrency
+
+Only long-running "top level" processes should invoke the Reloader, because if
+it determines a reload is needed, it will block until all other threads have
+completed any Executor invocations.
+
+If this were to occur in a "child" thread, with a waiting parent inside the
+Executor, it would cause an unavoidable deadlock: the reload must occur before
+the child thread is executed, but it cannot be safely performed while the parent
+thread is mid-execution. Child threads should use the Executor instead.
+
+Framework Behavior
+------------------
+
+The Rails framework components use these tools to manage their own concurrency
+needs too.
+
+`ActionDispatch::Executor` and `ActionDispatch::Reloader` are Rack middlewares
+that wraps the request with a supplied Executor or Reloader, respectively. They
+are automatically included in the default application stack. The Reloader will
+ensure any arriving HTTP request is served with a freshly-loaded copy of the
+application if any code changes have occurred.
+
+Active Job also wraps its job executions with the Reloader, loading the latest
+code to execute each job as it comes off the queue.
+
+Action Cable uses the Executor instead: because a Cable connection is linked to
+a specific instance of a class, it's not possible to reload for every arriving
+websocket message. Only the message handler is wrapped, though; a long-running
+Cable connection does not prevent a reload that's triggered by a new incoming
+request or job. Instead, Action Cable uses the Reloader's `before_class_unload`
+callback to disconnect all its connections. When the client automatically
+reconnects, it will be speaking to the new version of the code.
+
+The above are the entry points to the framework, so they are responsible for
+ensuring their respective threads are protected, and deciding whether a reload
+is necessary. Other components only need to use the Executor when they spawn
+additional threads.
+
+### Configuration
+
+The Reloader only checks for file changes when `cache_classes` is false and
+`reload_classes_only_on_change` is true (which is the default in the
+`development` environment).
+
+When `cache_classes` is true (in `production`, by default), the Reloader is only
+a pass-through to the Executor.
+
+The Executor always has important work to do, like database connection
+management. When `cache_classes` and `eager_load` are both true (`production`),
+no autoloading or class reloading will occur, so it does not need the Load
+Interlock. If either of those are false (`development`), then the Executor will
+use the Load Interlock to ensure constants are only loaded when it is safe.
+
+Load Interlock
+--------------
+
+The Load Interlock allows autoloading and reloading to be enabled in a
+multi-threaded runtime environment.
+
+When one thread is performing an autoload by evaluating the class definition
+from the appropriate file, it is important no other thread encounters a
+reference to the partially-defined constant.
+
+Similarly, it is only safe to perform an unload/reload when no application code
+is in mid-execution: after the reload, the `User` constant, for example, may
+point to a different class. Without this rule, a poorly-timed reload would mean
+`User.new.class == User`, or even `User == User`, could be false.
+
+Both of these constraints are addressed by the Load Interlock. It keeps track of
+which threads are currently running application code, loading a class, or
+unloading autoloaded constants.
+
+Only one thread may load or unload at a time, and to do either, it must wait
+until no other threads are running application code. If a thread is waiting to
+perform a load, it doesn't prevent other threads from loading (in fact, they'll
+cooperate, and each perform their queued load in turn, before all resuming
+running together).
+
+### `permit_concurrent_loads`
+
+The Executor automatically acquires a `running` lock for the duration of its
+block, and autoload knows when to upgrade to a `load` lock, and switch back to
+`running` again afterwards.
+
+Other blocking operations performed inside the Executor block (which includes
+all application code), however, can needlessly retain the `running` lock. If
+another thread encounters a constant it must autoload, this can cause a
+deadlock.
+
+For example, assuming `User` is not yet loaded, the following will deadlock:
+
+```ruby
+Rails.application.executor.wrap do
+ th = Thread.new do
+ Rails.application.executor.wrap do
+ User # inner thread waits here; it cannot load
+ # User while another thread is running
+ end
+ end
+
+ th.join # outer thread waits here, holding 'running' lock
+end
+```
+
+To prevent this deadlock, the outer thread can `permit_concurrent_loads`. By
+calling this method, the thread guarantees it will not dereference any
+possibly-autoloaded constant inside the supplied block. The safest way to meet
+that promise is to put it as close as possible to the blocking call:
+
+```ruby
+Rails.application.executor.wrap do
+ th = Thread.new do
+ Rails.application.executor.wrap do
+ User # inner thread can acquire the 'load' lock,
+ # load User, and continue
+ end
+ end
+
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ th.join # outer thread waits here, but has no lock
+ end
+end
+```
+
+Another example, using Concurrent Ruby:
+
+```ruby
+Rails.application.executor.wrap do
+ futures = 3.times.collect do |i|
+ Concurrent::Future.execute do
+ Rails.application.executor.wrap do
+ # do work here
+ end
+ end
+ end
+
+ values = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ futures.collect(&:value)
+ end
+end
+```
+
+
+### ActionDispatch::DebugLocks
+
+If your application is deadlocking and you think the Load Interlock may be
+involved, you can temporarily add the ActionDispatch::DebugLocks middleware to
+`config/application.rb`:
+
+```ruby
+config.middleware.insert_before Rack::Sendfile,
+ ActionDispatch::DebugLocks
+```
+
+If you then restart the application and re-trigger the deadlock condition,
+`/rails/locks` will show a summary of all threads currently known to the
+interlock, which lock level they are holding or awaiting, and their current
+backtrace.
+
+Generally a deadlock will be caused by the interlock conflicting with some other
+external lock or blocking I/O call. Once you find it, you can wrap it with
+`permit_concurrent_loads`.
+
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index d932fc8d8f..c2fe012eeb 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -1,7 +1,7 @@
**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
-A Guide for Upgrading Ruby on Rails
-===================================
+Upgrading Ruby on Rails
+=======================
This guide provides steps to be followed when you upgrade your applications to a newer version of Ruby on Rails. These steps are also available in individual release guides.
@@ -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 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).
@@ -35,6 +35,7 @@ You can find a list of all released Rails versions [here](https://rubygems.org/g
Rails generally stays close to the latest released Ruby version when it's released:
+* Rails 6 requires Ruby 2.4.1 or newer.
* Rails 5 requires Ruby 2.2.2 or newer.
* Rails 4 prefers Ruby 2.0 and requires 1.9.3 or newer.
* Rails 3.2.x is the last branch to support Ruby 1.8.7.
@@ -45,7 +46,7 @@ TIP: Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterp
### The Update Task
Rails provides the `app:update` task (`rake rails:update` on 4.2 and earlier). After updating the Rails version
-in the Gemfile, run this task.
+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.
@@ -65,6 +66,38 @@ Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh]
Don't forget to review the difference, to see if there were any unexpected changes.
+Upgrading from Rails 5.2 to Rails 6.0
+-------------------------------------
+
+### Force SSL
+
+The `force_ssl` method on controllers has been deprecated and will be removed in
+Rails 6.1. You are encouraged to enable `config.force_ssl` to enforce HTTPS
+connections throughout your application. If you need to exempt certain endpoints
+from redirection, you can use `config.ssl_options` to configure that behavior.
+
+
+Upgrading from Rails 5.1 to Rails 5.2
+-------------------------------------
+
+For more information on changes made to Rails 5.2 please see the [release notes](5_2_release_notes.html).
+
+### Bootsnap
+
+Rails 5.2 adds bootsnap gem in the [newly generated app's Gemfile](https://github.com/rails/rails/pull/29313).
+The `app:update` task sets it up in `boot.rb`. If you want to use it, then add it in the Gemfile,
+otherwise change the `boot.rb` to not use bootsnap.
+
+### Expiry in signed or encrypted cookie is now embedded in the cookies values
+
+To improve security, Rails now embeds the expiry information also in encrypted or signed cookies value.
+
+This new embed information make those cookies incompatible with versions of Rails older than 5.2.
+
+If you require your cookies to be read by 5.1 and older, or you are still validating your 5.2 deploy and want
+to allow you to rollback set
+`Rails.application.config.action_dispatch.use_authenticated_cookie_encryption` to `false`.
+
Upgrading from Rails 5.0 to Rails 5.1
-------------------------------------
@@ -72,7 +105,7 @@ For more information on changes made to Rails 5.1 please see the [release notes]
### Top-level `HashWithIndifferentAccess` is soft-deprecated
-If your application uses the the top-level `HashWithIndifferentAccess` class, you
+If your application uses the top-level `HashWithIndifferentAccess` class, you
should slowly move your code to instead use `ActiveSupport::HashWithIndifferentAccess`.
It is only soft-deprecated, which means that your code will not break at the
@@ -179,7 +212,7 @@ See [#19034](https://github.com/rails/rails/pull/19034) for more details.
`assigns` and `assert_template` have been extracted to the `rails-controller-testing` gem. To
continue using these methods in your controller tests, add `gem 'rails-controller-testing'` to
-your Gemfile.
+your `Gemfile`.
If you are using Rspec for testing, please see the extra configuration required in the gem's
documentation.
@@ -212,7 +245,7 @@ true.
`ActiveModel::Serializers::Xml` has been extracted from Rails to the `activemodel-serializers-xml`
gem. To continue using XML serialization in your application, add `gem 'activemodel-serializers-xml'`
-to your Gemfile.
+to your `Gemfile`.
### Removed Support for Legacy `mysql` Database Adapter
@@ -278,7 +311,7 @@ You can now just call the dependency once with a wildcard.
### `ActionView::Helpers::RecordTagHelper` moved to external gem (record_tag_helper)
-`content_tag_for` and `div_for` have been removed in favor of just using `content_tag`. To continue using the older methods, add the `record_tag_helper` gem to your Gemfile:
+`content_tag_for` and `div_for` have been removed in favor of just using `content_tag`. To continue using the older methods, add the `record_tag_helper` gem to your `Gemfile`:
```ruby
gem 'record_tag_helper', '~> 1.0'
@@ -411,11 +444,11 @@ Upgrading from Rails 4.1 to Rails 4.2
### Web Console
-First, add `gem 'web-console', '~> 2.0'` to the `:development` group in your Gemfile and run `bundle install` (it won't have been included when you upgraded Rails). Once it's been installed, you can simply drop a reference to the console helper (i.e., `<%= console %>`) into any view you want to enable it for. A console will also be provided on any error page you view in your development environment.
+First, add `gem 'web-console', '~> 2.0'` to the `:development` group in your `Gemfile` and run `bundle install` (it won't have been included when you upgraded Rails). Once it's been installed, you can simply drop a reference to the console helper (i.e., `<%= console %>`) into any view you want to enable it for. A console will also be provided on any error page you view in your development environment.
### Responders
-`respond_with` and the class-level `respond_to` methods have been extracted to the `responders` gem. To use them, simply add `gem 'responders', '~> 2.0'` to your Gemfile. Calls to `respond_with` and `respond_to` (again, at the class level) will no longer work without having included the `responders` gem in your dependencies:
+`respond_with` and the class-level `respond_to` methods have been extracted to the `responders` gem. To use them, simply add `gem 'responders', '~> 2.0'` to your `Gemfile`. Calls to `respond_with` and `respond_to` (again, at the class level) will no longer work without having included the `responders` gem in your dependencies:
```ruby
# app/controllers/users_controller.rb
@@ -559,7 +592,7 @@ Read the [gem's readme](https://github.com/rails/rails-html-sanitizer) for more
The documentation for `PermitScrubber` and `TargetScrubber` explains how you
can gain complete control over when and how elements should be stripped.
-If your application needs to use the old sanitizer implementation, include `rails-deprecated_sanitizer` in your Gemfile:
+If your application needs to use the old sanitizer implementation, include `rails-deprecated_sanitizer` in your `Gemfile`:
```ruby
gem 'rails-deprecated_sanitizer'
@@ -567,7 +600,7 @@ gem 'rails-deprecated_sanitizer'
### Rails DOM Testing
-The [`TagAssertions` module](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/TagAssertions.html) (containing methods such as `assert_tag`), [has been deprecated](https://github.com/rails/rails/blob/6061472b8c310158a2a2e8e9a6b81a1aef6b60fe/actionpack/lib/action_dispatch/testing/assertions/dom.rb) in favor of the `assert_select` methods from the `SelectorAssertions` module, which has been extracted into the [rails-dom-testing gem](https://github.com/rails/rails-dom-testing).
+The [`TagAssertions` module](http://api.rubyonrails.org/v4.1/classes/ActionDispatch/Assertions/TagAssertions.html) (containing methods such as `assert_tag`), [has been deprecated](https://github.com/rails/rails/blob/6061472b8c310158a2a2e8e9a6b81a1aef6b60fe/actionpack/lib/action_dispatch/testing/assertions/dom.rb) in favor of the `assert_select` methods from the `SelectorAssertions` module, which has been extracted into the [rails-dom-testing gem](https://github.com/rails/rails-dom-testing).
### Masked Authenticity Tokens
@@ -617,7 +650,7 @@ migration DSL counterpart.
The migration procedure is as follows:
-1. remove `gem "foreigner"` from the Gemfile.
+1. remove `gem "foreigner"` from the `Gemfile`.
2. run `bundle install`.
3. run `bin/rake db:schema:dump`.
4. make sure that `db/schema.rb` contains every foreign key definition with
@@ -648,7 +681,7 @@ xhr :get, :index, format: :js
to explicitly test an `XmlHttpRequest`.
-Note: Your own `<script>` tags are treated as cross-origin and blocked by
+NOTE: Your own `<script>` tags are treated as cross-origin and blocked by
default, too. If you really mean to load JavaScript from `<script>` tags,
you must now explicitly skip CSRF protection on those actions.
@@ -769,7 +802,7 @@ and has been removed from Rails.
If your application currently depends on MultiJSON directly, you have a few options:
-1. Add 'multi_json' to your Gemfile. Note that this might cease to work in the future
+1. Add 'multi_json' to your `Gemfile`. Note that this might cease to work in the future
2. Migrate away from MultiJSON by using `obj.to_json`, and `JSON.parse(str)` instead.
@@ -810,7 +843,7 @@ part of the rewrite, the following features have been removed from the encoder:
If your application depends on one of these features, you can get them back by
adding the [`activesupport-json_encoder`](https://github.com/rails/activesupport-json_encoder)
-gem to your Gemfile.
+gem to your `Gemfile`.
#### JSON representation of Time objects
@@ -1135,8 +1168,8 @@ full support for the last few changes in the specification.
### Gemfile
-Rails 4.0 removed the `assets` group from Gemfile. You'd need to remove that
-line from your Gemfile when upgrading. You should also update your application
+Rails 4.0 removed the `assets` group from `Gemfile`. You'd need to remove that
+line from your `Gemfile` when upgrading. You should also update your application
file (in `config/application.rb`):
```ruby
@@ -1147,7 +1180,7 @@ Bundler.require(*Rails.groups)
### vendor/plugins
-Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must replace any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
+Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must replace any plugins by extracting them to gems and adding them to your `Gemfile`. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
### Active Record
@@ -1214,7 +1247,7 @@ end
### Active Resource
-Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your Gemfile.
+Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your `Gemfile`.
### Active Model
@@ -1414,7 +1447,7 @@ config.active_record.mass_assignment_sanitizer = :strict
### vendor/plugins
-Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. While it's not strictly necessary as part of a Rails 3.2 upgrade, you can start replacing any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
+Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. While it's not strictly necessary as part of a Rails 3.2 upgrade, you can start replacing any plugins by extracting them to gems and adding them to your `Gemfile`. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
### Active Record
diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md
index 098366ec1b..a922bdc16b 100644
--- a/guides/source/working_with_javascript_in_rails.md
+++ b/guides/source/working_with_javascript_in_rails.md
@@ -174,7 +174,7 @@ passing the `:local` option `form_with`.
This will generate the following HTML:
```html
-<form action="/articles" method="post" data-remote="true">
+<form action="/articles" accept-charset="UTF-8" method="post" data-remote="true">
...
</form>
```
@@ -188,15 +188,20 @@ bind to the `ajax:success` event. On failure, use `ajax:error`. Check it out:
```coffeescript
$(document).ready ->
- $("#new_article").on("ajax:success", (e, data, status, xhr) ->
+ $("#new_article").on("ajax:success", (event) ->
+ [data, status, xhr] = event.detail
$("#new_article").append xhr.responseText
- ).on "ajax:error", (e, xhr, status, error) ->
+ ).on "ajax:error", (event) ->
$("#new_article").append "<p>ERROR</p>"
```
Obviously, you'll want to be a bit more sophisticated than that, but it's a
start.
+NOTE: As of Rails 5.1 and the new `rails-ujs`, the parameters `data, status, xhr`
+have been bundled into `event.detail`. For information about the previously used
+`jquery-ujs` in Rails 5 and earlier, read the [`jquery-ujs` wiki](https://github.com/rails/jquery-ujs/wiki/ajax).
+
#### link_to
[`link_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to)
@@ -225,7 +230,7 @@ and write some CoffeeScript like this:
```coffeescript
$ ->
- $("a[data-remote]").on "ajax:success", (e, data, status, xhr) ->
+ $("a[data-remote]").on "ajax:success", (event) ->
alert "The article was deleted."
```
@@ -343,39 +348,6 @@ This generates a form with:
<input data-disable-with="Saving..." type="submit">
```
-Dealing with Ajax events
-------------------------
-
-Here are the different events that are fired when you deal with elements
-that have a `data-remote` attribute:
-
-NOTE: All handlers bound to these events are always passed the event object as the
-first argument. The table below describes the extra parameters passed after the
-event argument. For example, if the extra parameters are listed as `xhr, settings`,
-then to access them, you would define your handler with `function(event, xhr, settings)`.
-
-| Event name | Extra parameters | Fired |
-|---------------------|------------------|-------------------------------------------------------------|
-| `ajax:before` | | Before the whole ajax business, aborts if stopped. |
-| `ajax:beforeSend` | xhr, options | Before the request is sent, aborts if stopped. |
-| `ajax:send` | xhr | When the request is sent. |
-| `ajax:success` | xhr, status, err | After completion, if the response was a success. |
-| `ajax:error` | xhr, status, err | After completion, if the response was an error. |
-| `ajax:complete` | xhr, status | After the request has been completed, no matter the outcome.|
-| `ajax:aborted:file` | elements | If there are non-blank file inputs, aborts if stopped. |
-
-### Stoppable events
-
-If you stop `ajax:before` or `ajax:beforeSend` by returning false from the
-handler method, the Ajax request will never take place. The `ajax:before` event
-is also useful for manipulating form data before serialization. The
-`ajax:beforeSend` event is also useful for adding custom request headers.
-
-If you stop the `ajax:aborted:file` event, the default behavior of allowing the
-browser to submit the form via normal means (i.e. non-Ajax submission) will be
-canceled and the form will not be submitted at all. This is useful for
-implementing your own Ajax file upload workaround.
-
### Rails-ujs event handlers
Rails 5.1 introduced rails-ujs and dropped jQuery as a dependency.
@@ -401,10 +373,28 @@ Example usage:
```html
document.body.addEventListener('ajax:success', function(event) {
var detail = event.detail;
- var data = detail[0], status = detail[1], xhr = detail[2];
+ var data = detail[0], status = detail[1], xhr = detail[2];
})
```
+NOTE: As of Rails 5.1 and the new `rails-ujs`, the parameters `data, status, xhr`
+have been bundled into `event.detail`. For information about the previously used
+`jquery-ujs` in Rails 5 and earlier, read the [`jquery-ujs` wiki](https://github.com/rails/jquery-ujs/wiki/ajax).
+
+### Stoppable events
+You can stop execution of the Ajax request by running `event.preventDefault()`
+from the handlers methods `ajax:before` or `ajax:beforeSend`.
+The `ajax:before` event can manipulate form data before serialization and the
+`ajax:beforeSend` event is useful for adding custom request headers.
+
+If you stop the `ajax:aborted:file` event, the default behavior of allowing the
+browser to submit the form via normal means (i.e. non-Ajax submission) will be
+canceled and the form will not be submitted at all. This is useful for
+implementing your own Ajax file upload workaround.
+
+Note, you should use `return false` to prevent event for `jquery-ujs` and
+`e.preventDefault()` for `rails-ujs`
+
Server-Side Concerns
--------------------
@@ -504,7 +494,7 @@ replace the entire `<body>` of the page with the `<body>` of the response. It
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,
+The only thing you have to do to enable Turbolinks is have it in your `Gemfile`,
and put `//= require turbolinks` in your JavaScript manifest, which is usually
`app/assets/javascripts/application.js`.
diff --git a/rails.gemspec b/rails.gemspec
index 4b57377871..709ce642f3 100644
--- a/rails.gemspec
+++ b/rails.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Full-stack web application framework."
s.description = "Ruby on Rails is a full-stack web framework optimized for programmer happiness and sustainable productivity. It encourages beautiful code by favoring convention over configuration."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.required_rubygems_version = ">= 1.8.11"
s.license = "MIT"
diff --git a/railties/.gitignore b/railties/.gitignore
index 80dd262d2f..c08562e016 100644
--- a/railties/.gitignore
+++ b/railties/.gitignore
@@ -1 +1,5 @@
-log/
+/log/
+/test/500.html
+/test/fixtures/tmp/
+/test/initializer/root/log/
+/tmp/
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index ff440b7939..a4d4a87a8b 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,143 +1,68 @@
-* Add `mini_magick` to default `Gemfile` as comment.
-
- *Yoshiyuki Hirano*
-
-* Derive `secret_key_base` from the app name in development and test environments.
-
- Spares away needless secret configs.
-
- *DHH*, *Kasper Timm Hansen*
-
-* Support multiple versions arguments for `gem` method of Generators.
-
- *Yoshiyuki Hirano*
-
-* Add `--skip-yarn` option to the plugin generator.
-
- *bogdanvlviv*
-
-* Optimize routes indentation.
-
- *Yoshiyuki Hirano*
-
-* Optimize indentation for generator actions.
-
- *Yoshiyuki Hirano*
-
-* Skip unused components when running `bin/rails` in Rails plugin.
-
- *Yoshiyuki Hirano*
-
-* Add `git_source` to `Gemfile` for plugin generator.
-
- *Yoshiyuki Hirano*
-
-* Add `--skip-action-cable` option to the plugin generator.
-
- *bogdanvlviv*
-
-* Deprecate support of use `Rails::Application` subclass to start Rails server.
-
- *Yuji Yaginuma*
-
-* Add `ruby x.x.x` version to `Gemfile` and create `.ruby-version`
- root file containing the current Ruby version when new Rails applications are
- created.
-
- *Alberto Almagro*
-
-* Support `-` as a platform-agnostic way to run a script from stdin with
- `rails runner`
-
- *Cody Cutrer*
-
-* Add `bootsnap` to default `Gemfile`.
-
- *Burke Libbey*
-
-* Properly expand shortcuts for environment's name running the `console`
- and `dbconsole` commands.
-
- *Robin Dupret*
-
-* Passing the environment's name as a regular argument to the
- `rails dbconsole` and `rails console` commands is deprecated.
- The `-e` option should be used instead.
+* Make the master.key file read-only for the owner upon generation on
+ POSIX-compliant systems.
Previously:
- $ bin/rails dbconsole production
+ $ ls -l config/master.key
+ -rw-r--r-- 1 owner group 32 Jan 1 00:00 master.key
Now:
- $ bin/rails dbconsole -e production
-
- *Robin Dupret*, *Kasper Timm Hansen*
-
-* Allow passing a custom connection name to the `rails dbconsole`
- command when using a 3-level database configuration.
-
- $ bin/rails dbconsole -c replica
-
- *Robin Dupret*, *Jeremy Daer*
-
-* Skip unused components when running `bin/rails app:update`.
-
- If the initial app generation skipped Action Cable, Active Record etc.,
- the update task honors those skips too.
-
- *Yuji Yaginuma*
-
-* Make Rails' test runner work better with minitest plugins.
-
- By demoting the Rails test runner to just another minitest plugin —
- and thereby not eager loading it — we can co-exist much better with
- other minitest plugins such as pride and minitest-focus.
+ $ ls -l config/master.key
+ -rw------- 1 owner group 32 Jan 1 00:00 master.key
- *Kasper Timm Hansen*
+ Fixes #32604.
-* Load environment file in `dbconsole` command.
+ *Jose Luis Duran*
- Fixes #29717.
+* Deprecate support for using the `HOST` environment to specify the server IP.
- *Yuji Yaginuma*
+ The `BINDING` environment should be used instead.
-* Add `rails secrets:show` command.
+ Fixes #29516.
*Yuji Yaginuma*
-* Allow mounting the same engine several times in different locations.
-
- Fixes #20204.
-
- *David Rodríguez*
+* Deprecate passing Rack server name as a regular argument to `rails server`.
-* Clear screenshot files in `tmp:clear` task.
-
- *Yuji Yaginuma*
+ Previously:
-* Add `railtie.rb` to the plugin generator
+ $ bin/rails server thin
- *Tsukuru Tanimichi*
+ There wasn't an explicit option for the Rack server to use, now we have the
+ `--using` option with the `-u` short switch.
-* Deprecate `capify!` method in generators and templates.
+ Now:
- *Yuji Yaginuma*
+ $ bin/rails server -u thin
-* Allow irb options to be passed from `rails console` command.
+ This change also improves the error message if a missing or mistyped rack
+ server is given.
- Fixes #28988.
+ *Genadi Samokovarov*
- *Yuji Yaginuma*
+* Add "rails routes --expanded" option to output routes in expanded mode like
+ "psql --expanded". Result looks like:
-* Added a shared section to `config/database.yml` that will be loaded for all environments.
+ ```
+ $ rails routes --expanded
+ --[ Route 1 ]------------------------------------------------------------
+ Prefix | high_scores
+ Verb | GET
+ URI | /high_scores(.:format)
+ Controller#Action | high_scores#index
+ --[ Route 2 ]------------------------------------------------------------
+ Prefix | new_high_score
+ Verb | GET
+ URI | /high_scores/new(.:format)
+ Controller#Action | high_scores#new
+ ```
- *Pierre Schambacher*
+ *Benoit Tigeot*
-* Namespace error pages' CSS selectors to stop the styles from bleeding into other pages
- when using Turbolinks.
+* Rails 6 requires Ruby 2.4.1 or newer.
- *Jan Krutisch*
+ *Jeremy Daer*
-Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/railties/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/railties/CHANGELOG.md) for previous changes.
diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE
index f9e4444f07..cce00cbc3a 100644
--- a/railties/MIT-LICENSE
+++ b/railties/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2017 David Heinemeier Hansson
+Copyright (c) 2004-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc
index c70a9f0ba0..a4a4b6b235 100644
--- a/railties/RDOC_MAIN.rdoc
+++ b/railties/RDOC_MAIN.rdoc
@@ -1,4 +1,6 @@
-== Welcome to \Rails
+= Welcome to \Rails
+
+== What's \Rails
\Rails is a web-application framework that includes everything needed to
create database-backed web applications according to the
@@ -6,43 +8,48 @@ create database-backed web applications according to the
pattern.
Understanding the MVC pattern is key to understanding \Rails. MVC divides your
-application into three layers, each with a specific responsibility.
+application into three layers: Model, View, and Controller, each with a specific responsibility.
+
+== Model layer
-The <em>Model layer</em> represents your domain model (such as Account, Product,
-Person, Post, etc.) and encapsulates the business logic that is specific to
+The <em><b>Model layer</b></em> represents the domain model (such as Account, Product,
+Person, Post, etc.) and encapsulates the business logic specific to
your application. In \Rails, database-backed model classes are derived from
-ActiveRecord::Base. Active Record allows you to present the data from
+<tt>ActiveRecord::Base</tt>. {Active Record}[link:files/activerecord/README_rdoc.html] allows you to present the data from
database rows as objects and embellish these data objects with business logic
-methods. You can read more about Active Record in its {README}[link:files/activerecord/README_rdoc.html].
-Although most \Rails models are backed by a database, models can also be ordinary
+methods. Although most \Rails models are backed by a database, models can also be ordinary
Ruby classes, or Ruby classes that implement a set of interfaces as provided by
-the Active Model module. You can read more about Active Model in its {README}[link:files/activemodel/README_rdoc.html].
+the {Active Model}[link:files/activemodel/README_rdoc.html] module.
+
+== Controller layer
-The <em>Controller layer</em> is responsible for handling incoming HTTP requests and
+The <em><b>Controller layer</b></em> is responsible for handling incoming HTTP requests and
providing a suitable response. Usually this means returning \HTML, but \Rails controllers
can also generate XML, JSON, PDFs, mobile-specific views, and more. Controllers load and
manipulate models, and render view templates in order to generate the appropriate HTTP response.
In \Rails, incoming requests are routed by Action Dispatch to an appropriate controller, and
-controller classes are derived from ActionController::Base. Action Dispatch and Action Controller
-are bundled together in Action Pack. You can read more about Action Pack in its
-{README}[link:files/actionpack/README_rdoc.html].
+controller classes are derived from <tt>ActionController::Base</tt>. Action Dispatch and Action Controller
+are bundled together in {Action Pack}[link:files/actionpack/README_rdoc.html].
-The <em>View layer</em> is composed of "templates" that are responsible for providing
+== View layer
+
+The <em><b>View layer</b></em> is composed of "templates" that are responsible for providing
appropriate representations of your application's resources. Templates can
come in a variety of formats, but most view templates are \HTML with embedded
Ruby code (ERB files). Views are typically rendered to generate a controller response,
-or to generate the body of an email. In \Rails, View generation is handled by Action View.
-You can read more about Action View in its {README}[link:files/actionview/README_rdoc.html].
+or to generate the body of an email. In \Rails, View generation is handled by {Action View}[link:files/actionview/README_rdoc.html].
+
+== Frameworks and libraries
-Active Record, Active Model, Action Pack, and Action View can each be used independently outside \Rails.
-In addition to that, \Rails also comes with Action Mailer ({README}[link:files/actionmailer/README_rdoc.html]), a library
-to generate and send emails; Active Job ({README}[link:files/activejob/README_md.html]), a
+{Active Record}[link:files/activerecord/README_rdoc.html], {Active Model}[link:files/activemodel/README_rdoc.html],
+{Action Pack}[link:files/actionpack/README_rdoc.html], and {Action View}[link:files/actionview/README_rdoc.html] can each be used independently outside \Rails.
+In addition to that, \Rails also comes with {Action Mailer}[link:files/actionmailer/README_rdoc.html], a library
+to generate and send emails; {Active Job}[link:files/activejob/README_md.html], a
framework for declaring jobs and making them run on a variety of queueing
-backends; Action Cable ({README}[link:files/actioncable/README_md.html]), a framework to
-integrate WebSockets with a \Rails application;
-Active Storage ({README}[link:files/activestorage/README_md.html]), a library to attach cloud
-and local files to \Rails applications;
-and Active Support ({README}[link:files/activesupport/README_rdoc.html]), a collection
+backends; {Action Cable}[link:files/actioncable/README_md.html], a framework to
+integrate WebSockets with a \Rails application; {Active Storage}[link:files/activestorage/README_md.html],
+a library to attach cloud and local files to \Rails applications;
+and {Active Support}[link:files/activesupport/README_rdoc.html], a collection
of utility classes and standard library extensions that are useful for \Rails,
and may also be used independently outside \Rails.
diff --git a/railties/lib/minitest/rails_plugin.rb b/railties/lib/minitest/rails_plugin.rb
index 6901b0bbc8..6486fa1798 100644
--- a/railties/lib/minitest/rails_plugin.rb
+++ b/railties/lib/minitest/rails_plugin.rb
@@ -13,7 +13,7 @@ module Minitest
end
def self.plugin_rails_options(opts, options)
- Rails::TestUnit::Runner.attach_before_load_options(opts)
+ ::Rails::TestUnit::Runner.attach_before_load_options(opts)
opts.on("-b", "--backtrace", "Show the complete backtrace") do
options[:full_backtrace] = true
@@ -43,10 +43,15 @@ module Minitest
Minitest.backtrace_filter = ::Rails.backtrace_cleaner if ::Rails.respond_to?(:backtrace_cleaner)
end
+ # Suppress summary reports when outputting inline rerun snippets.
+ if reporter.reporters.reject! { |reporter| reporter.kind_of?(SummaryReporter) }
+ reporter << SuppressedSummaryReporter.new(options[:io], options)
+ end
+
# Replace progress reporter for colors.
- reporter.reporters.delete_if { |reporter| reporter.kind_of?(SummaryReporter) || reporter.kind_of?(ProgressReporter) }
- reporter << SuppressedSummaryReporter.new(options[:io], options)
- reporter << ::Rails::TestUnitReporter.new(options[:io], options)
+ if reporter.reporters.reject! { |reporter| reporter.kind_of?(ProgressReporter) }
+ reporter << ::Rails::TestUnitReporter.new(options[:io], options)
+ end
end
# Backwardscompatibility with Rails 5.0 generated plugin test scripts
diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb
index 04bb9ba94a..092105d502 100644
--- a/railties/lib/rails.rb
+++ b/railties/lib/rails.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "rails/ruby_version_check"
+require "rails/ruby_version_check"
require "pathname"
@@ -11,8 +11,8 @@ require "active_support/core_ext/module/delegation"
require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/object/blank"
-require_relative "rails/application"
-require_relative "rails/version"
+require "rails/application"
+require "rails/version"
require "active_support/railtie"
require "action_dispatch/railtie"
@@ -50,7 +50,7 @@ module Rails
def backtrace_cleaner
@backtrace_cleaner ||= begin
# Relies on Active Support, so we have to lazy load to postpone definition until Active Support has been loaded
- require_relative "rails/backtrace_cleaner"
+ require "rails/backtrace_cleaner"
Rails::BacktraceCleaner.new
end
end
diff --git a/railties/lib/rails/all.rb b/railties/lib/rails/all.rb
index e55b2e2433..f5dccd2381 100644
--- a/railties/lib/rails/all.rb
+++ b/railties/lib/rails/all.rb
@@ -4,12 +4,12 @@ require "rails"
%w(
active_record/railtie
+ active_storage/engine
action_controller/railtie
action_view/railtie
action_mailer/railtie
active_job/railtie
action_cable/engine
- active_storage/engine
rails/test_unit/railtie
sprockets/railtie
).each do |railtie|
diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb
index 184f5b14f1..e7f0557584 100644
--- a/railties/lib/rails/api/task.rb
+++ b/railties/lib/rails/api/task.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "rdoc/task"
-require_relative "generator"
+require "rails/api/generator"
module Rails
module API
diff --git a/railties/lib/rails/app_loader.rb b/railties/lib/rails/app_loader.rb
index 3e9b3bd4bb..20eb75d95c 100644
--- a/railties/lib/rails/app_loader.rb
+++ b/railties/lib/rails/app_loader.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "pathname"
-require_relative "version"
+require "rails/version"
module Rails
module AppLoader # :nodoc:
@@ -56,7 +56,7 @@ EOS
$stderr.puts(BUNDLER_WARNING)
Object.const_set(:APP_PATH, File.expand_path("config/application", Dir.pwd))
require File.expand_path("../boot", APP_PATH)
- require_relative "commands"
+ require "rails/commands"
break
end
end
diff --git a/railties/lib/rails/app_updater.rb b/railties/lib/rails/app_updater.rb
index c2436a69f9..a076d082d5 100644
--- a/railties/lib/rails/app_updater.rb
+++ b/railties/lib/rails/app_updater.rb
@@ -21,11 +21,12 @@ module Rails
private
def generator_options
options = { api: !!Rails.application.config.api_only, update: true }
- options[:skip_active_record] = !defined?(ActiveRecord::Railtie)
- options[:skip_action_mailer] = !defined?(ActionMailer::Railtie)
- options[:skip_action_cable] = !defined?(ActionCable::Engine)
- options[:skip_sprockets] = !defined?(Sprockets::Railtie)
- options[:skip_puma] = !defined?(Puma)
+ options[:skip_active_record] = !defined?(ActiveRecord::Railtie)
+ options[:skip_active_storage] = !defined?(ActiveStorage::Engine) || !defined?(ActiveRecord::Railtie)
+ options[:skip_action_mailer] = !defined?(ActionMailer::Railtie)
+ options[:skip_action_cable] = !defined?(ActionCable::Engine)
+ options[:skip_sprockets] = !defined?(Sprockets::Railtie)
+ options[:skip_puma] = !defined?(Puma)
options
end
end
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index abfec90b6d..e346d5cc3a 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -6,8 +6,9 @@ require "active_support/core_ext/object/blank"
require "active_support/key_generator"
require "active_support/message_verifier"
require "active_support/encrypted_configuration"
-require_relative "engine"
-require_relative "secrets"
+require "active_support/deprecation"
+require "rails/engine"
+require "rails/secrets"
module Rails
# An Engine with the responsibility of coordinating the whole boot process.
@@ -173,8 +174,9 @@ module Rails
# team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220
@caching_key_generator ||=
if secret_key_base
- ActiveSupport::CachingKeyGenerator.new \
+ ActiveSupport::CachingKeyGenerator.new(
ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
+ )
else
ActiveSupport::LegacyKeyGenerator.new(secrets.secret_token)
end
@@ -259,8 +261,15 @@ module Rails
"action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt,
"action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt,
"action_dispatch.authenticated_encrypted_cookie_salt" => config.action_dispatch.authenticated_encrypted_cookie_salt,
+ "action_dispatch.use_authenticated_cookie_encryption" => config.action_dispatch.use_authenticated_cookie_encryption,
+ "action_dispatch.encrypted_cookie_cipher" => config.action_dispatch.encrypted_cookie_cipher,
+ "action_dispatch.signed_cookie_digest" => config.action_dispatch.signed_cookie_digest,
"action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer,
- "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest
+ "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest,
+ "action_dispatch.cookies_rotations" => config.action_dispatch.cookies_rotations,
+ "action_dispatch.content_security_policy" => config.content_security_policy,
+ "action_dispatch.content_security_policy_report_only" => config.content_security_policy_report_only,
+ "action_dispatch.content_security_policy_nonce_generator" => config.content_security_policy_nonce_generator
)
end
end
@@ -394,6 +403,12 @@ module Rails
# Fallback to config.secret_token if secrets.secret_token isn't set
secrets.secret_token ||= config.secret_token
+ if secrets.secret_token.present?
+ ActiveSupport::Deprecation.warn(
+ "`secrets.secret_token` is deprecated in favor of `secret_key_base` and will be removed in Rails 6.0."
+ )
+ end
+
secrets
end
end
@@ -412,21 +427,55 @@ module Rails
# the correct place to store it is in the encrypted credentials file.
def secret_key_base
if Rails.env.test? || Rails.env.development?
- Digest::MD5.hexdigest self.class.name
+ secrets.secret_key_base || Digest::MD5.hexdigest(self.class.name)
else
- validate_secret_key_base \
+ validate_secret_key_base(
ENV["SECRET_KEY_BASE"] || credentials.secret_key_base || secrets.secret_key_base
+ )
end
end
- # Decrypts the credentials hash as kept in `config/credentials.yml.enc`. This file is encrypted with
- # the Rails master key, which is either taken from ENV["RAILS_MASTER_KEY"] or from loading
- # `config/master.key`.
+ # Decrypts the credentials hash as kept in +config/credentials.yml.enc+. This file is encrypted with
+ # the Rails master key, which is either taken from <tt>ENV["RAILS_MASTER_KEY"]</tt> or from loading
+ # +config/master.key+.
def credentials
- @credentials ||= ActiveSupport::EncryptedConfiguration.new \
- config_path: Rails.root.join("config/credentials.yml.enc"),
- key_path: Rails.root.join("config/master.key"),
- env_key: "RAILS_MASTER_KEY"
+ @credentials ||= encrypted("config/credentials.yml.enc")
+ end
+
+ # Shorthand to decrypt any encrypted configurations or files.
+ #
+ # For any file added with <tt>bin/rails encrypted:edit</tt> call +read+ to decrypt
+ # the file with the master key.
+ # The master key is either stored in +config/master.key+ or <tt>ENV["RAILS_MASTER_KEY"]</tt>.
+ #
+ # Rails.application.encrypted("config/mystery_man.txt.enc").read
+ # # => "We've met before, haven't we?"
+ #
+ # It's also possible to interpret encrypted YAML files with +config+.
+ #
+ # Rails.application.encrypted("config/credentials.yml.enc").config
+ # # => { next_guys_line: "I don't think so. Where was it you think we met?" }
+ #
+ # Any top-level configs are also accessible directly on the return value:
+ #
+ # Rails.application.encrypted("config/credentials.yml.enc").next_guys_line
+ # # => "I don't think so. Where was it you think we met?"
+ #
+ # The files or configs can also be encrypted with a custom key. To decrypt with
+ # a key in the +ENV+, use:
+ #
+ # Rails.application.encrypted("config/special_tokens.yml.enc", env_key: "SPECIAL_TOKENS")
+ #
+ # Or to decrypt with a file, that should be version control ignored, relative to +Rails.root+:
+ #
+ # Rails.application.encrypted("config/special_tokens.yml.enc", key_path: "config/special_tokens.key")
+ def encrypted(path, key_path: "config/master.key", env_key: "RAILS_MASTER_KEY")
+ ActiveSupport::EncryptedConfiguration.new(
+ config_path: Rails.root.join(path),
+ key_path: Rails.root.join(key_path),
+ env_key: env_key,
+ raise_if_missing_key: config.require_master_key
+ )
end
def to_app #:nodoc:
@@ -464,7 +513,7 @@ module Rails
def run_tasks_blocks(app) #:nodoc:
railties.each { |r| r.run_tasks_blocks(app) }
super
- require_relative "tasks"
+ require "rails/tasks"
task :environment do
ActiveSupport.on_load(:before_initialize) { config.eager_load = false }
diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb
index c24d4573a9..e3c0759f95 100644
--- a/railties/lib/rails/application/bootstrap.rb
+++ b/railties/lib/rails/application/bootstrap.rb
@@ -4,7 +4,7 @@ require "fileutils"
require "active_support/notifications"
require "active_support/dependencies"
require "active_support/descendants_tracker"
-require_relative "../secrets"
+require "rails/secrets"
module Rails
class Application
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index b65289177f..bba573499d 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -2,8 +2,8 @@
require "active_support/core_ext/kernel/reporting"
require "active_support/file_update_checker"
-require_relative "../engine/configuration"
-require_relative "../source_annotation_extractor"
+require "rails/engine/configuration"
+require "rails/source_annotation_extractor"
module Rails
class Application
@@ -16,44 +16,50 @@ module Rails
:ssl_options, :public_file_server,
:session_options, :time_zone, :reload_classes_only_on_change,
:beginning_of_week, :filter_redirect, :x, :enable_dependency_loading,
- :read_encrypted_secrets, :log_level
+ :read_encrypted_secrets, :log_level, :content_security_policy_report_only,
+ :content_security_policy_nonce_generator, :require_master_key
- attr_reader :encoding, :api_only
+ attr_reader :encoding, :api_only, :loaded_config_version
def initialize(*)
super
- self.encoding = Encoding::UTF_8
- @allow_concurrency = nil
- @consider_all_requests_local = false
- @filter_parameters = []
- @filter_redirect = []
- @helpers_paths = []
- @public_file_server = ActiveSupport::OrderedOptions.new
- @public_file_server.enabled = true
- @public_file_server.index_name = "index"
- @force_ssl = false
- @ssl_options = {}
- @session_store = nil
- @time_zone = "UTC"
- @beginning_of_week = :monday
- @log_level = :debug
- @generators = app_generators
- @cache_store = [ :file_store, "#{root}/tmp/cache/" ]
- @railties_order = [:all]
- @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"]
- @reload_classes_only_on_change = true
- @file_watcher = ActiveSupport::FileUpdateChecker
- @exceptions_app = nil
- @autoflush_log = true
- @log_formatter = ActiveSupport::Logger::SimpleFormatter.new
- @eager_load = nil
- @secret_token = nil
- @secret_key_base = nil
- @api_only = false
- @debug_exception_response_format = nil
- @x = Custom.new
- @enable_dependency_loading = false
- @read_encrypted_secrets = false
+ self.encoding = Encoding::UTF_8
+ @allow_concurrency = nil
+ @consider_all_requests_local = false
+ @filter_parameters = []
+ @filter_redirect = []
+ @helpers_paths = []
+ @public_file_server = ActiveSupport::OrderedOptions.new
+ @public_file_server.enabled = true
+ @public_file_server.index_name = "index"
+ @force_ssl = false
+ @ssl_options = {}
+ @session_store = nil
+ @time_zone = "UTC"
+ @beginning_of_week = :monday
+ @log_level = :debug
+ @generators = app_generators
+ @cache_store = [ :file_store, "#{root}/tmp/cache/" ]
+ @railties_order = [:all]
+ @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"]
+ @reload_classes_only_on_change = true
+ @file_watcher = ActiveSupport::FileUpdateChecker
+ @exceptions_app = nil
+ @autoflush_log = true
+ @log_formatter = ActiveSupport::Logger::SimpleFormatter.new
+ @eager_load = nil
+ @secret_token = nil
+ @secret_key_base = nil
+ @api_only = false
+ @debug_exception_response_format = nil
+ @x = Custom.new
+ @enable_dependency_loading = false
+ @read_encrypted_secrets = false
+ @content_security_policy = nil
+ @content_security_policy_report_only = false
+ @content_security_policy_nonce_generator = nil
+ @require_master_key = false
+ @loaded_config_version = nil
end
def load_defaults(target_version)
@@ -71,7 +77,6 @@ module Rails
end
self.ssl_options = { hsts: { subdomains: true } }
-
when "5.1"
load_defaults "5.0"
@@ -82,7 +87,6 @@ module Rails
if respond_to?(:action_view)
action_view.form_with_generates_remote_forms = true
end
-
when "5.2"
load_defaults "5.1"
@@ -100,15 +104,27 @@ module Rails
if respond_to?(:active_support)
active_support.use_authenticated_message_encryption = true
+ active_support.use_sha1_digests = true
end
if respond_to?(:action_controller)
action_controller.default_protect_from_forgery = true
end
+ if respond_to?(:action_view)
+ action_view.form_with_generates_ids = true
+ end
+ when "6.0"
+ load_defaults "5.2"
+
+ if respond_to?(:action_view)
+ action_view.default_enforce_utf8 = false
+ end
else
raise "Unknown version #{target_version.to_s.inspect}"
end
+
+ @loaded_config_version = target_version
end
def encoding=(value)
@@ -150,6 +166,18 @@ module Rails
end
end
+ # Loads the database YAML without evaluating ERB. People seem to
+ # write ERB that makes the database configuration depend on
+ # Rails configuration. But we want Rails configuration (specifically
+ # `rake` and `rails` tasks) to be generated based on information in
+ # the database yaml, so we need a method that loads the database
+ # yaml *without* the context of the Rails application.
+ def load_database_yaml # :nodoc:
+ path = paths["config/database"].existent.first
+ return {} unless path
+ YAML.load_file(path.to_s)
+ end
+
# Loads and returns the entire raw configuration of database from
# values stored in <tt>config/database.yml</tt>.
def database_configuration
@@ -225,7 +253,15 @@ module Rails
end
def annotations
- SourceAnnotationExtractor::Annotation
+ Rails::SourceAnnotationExtractor::Annotation
+ end
+
+ def content_security_policy(&block)
+ if block_given?
+ @content_security_policy = ActionDispatch::ContentSecurityPolicy.new(&block)
+ else
+ @content_security_policy
+ end
end
class Custom #:nodoc:
diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb
index ea2273c1f2..433a7ab41f 100644
--- a/railties/lib/rails/application/default_middleware_stack.rb
+++ b/railties/lib/rails/application/default_middleware_stack.rb
@@ -63,9 +63,15 @@ module Rails
middleware.use ::ActionDispatch::Flash
end
+ unless config.api_only
+ middleware.use ::ActionDispatch::ContentSecurityPolicy::Middleware
+ end
+
middleware.use ::Rack::Head
middleware.use ::Rack::ConditionalGet
middleware.use ::Rack::ETag, "no-cache"
+
+ middleware.use ::Rack::TempfileReaper unless config.api_only
end
end
diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb
index 3d938be951..c4b188aeee 100644
--- a/railties/lib/rails/application/finisher.rb
+++ b/railties/lib/rails/application/finisher.rb
@@ -58,7 +58,7 @@ module Rails
end
# This needs to happen before eager load so it happens
- # in exactly the same point regardless of config.cache_classes
+ # in exactly the same point regardless of config.eager_load
initializer :run_prepare_callbacks do |app|
app.reloader.prepare!
end
diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb
index fa8793d81a..b3fe822218 100644
--- a/railties/lib/rails/application_controller.rb
+++ b/railties/lib/rails/application_controller.rb
@@ -4,6 +4,13 @@ class Rails::ApplicationController < ActionController::Base # :nodoc:
self.view_paths = File.expand_path("templates", __dir__)
layout "application"
+ before_action :disable_content_security_policy_nonce!
+
+ content_security_policy do |policy|
+ policy.script_src :unsafe_inline
+ policy.style_src :unsafe_inline
+ end
+
private
def require_local!
@@ -15,4 +22,8 @@ class Rails::ApplicationController < ActionController::Base # :nodoc:
def local_request?
Rails.application.config.consider_all_requests_local || request.local?
end
+
+ def disable_content_security_policy_nonce!
+ request.content_security_policy_nonce_generator = nil
+ end
end
diff --git a/railties/lib/rails/cli.rb b/railties/lib/rails/cli.rb
index 50a2ed30cf..e56e604fdc 100644
--- a/railties/lib/rails/cli.rb
+++ b/railties/lib/rails/cli.rb
@@ -1,15 +1,15 @@
# frozen_string_literal: true
-require_relative "app_loader"
+require "rails/app_loader"
# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::AppLoader.exec_app
-require_relative "ruby_version_check"
+require "rails/ruby_version_check"
Signal.trap("INT") { puts; exit(1) }
-require_relative "command"
+require "rails/command"
if ARGV.first == "plugin"
ARGV.shift
diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb
index 7ceb86198f..9c447c366f 100644
--- a/railties/lib/rails/code_statistics.rb
+++ b/railties/lib/rails/code_statistics.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "code_statistics_calculator"
+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
index 812e846837..6d99ac9936 100644
--- a/railties/lib/rails/command.rb
+++ b/railties/lib/rails/command.rb
@@ -4,7 +4,6 @@ require "active_support"
require "active_support/dependencies/autoload"
require "active_support/core_ext/enumerable"
require "active_support/core_ext/object/blank"
-require "active_support/core_ext/hash/transform_values"
require "thor"
@@ -12,6 +11,7 @@ module Rails
module Command
extend ActiveSupport::Autoload
+ autoload :Spellchecker
autoload :Behavior
autoload :Base
diff --git a/railties/lib/rails/command/base.rb b/railties/lib/rails/command/base.rb
index 8df4f98d3e..fa462ef7e9 100644
--- a/railties/lib/rails/command/base.rb
+++ b/railties/lib/rails/command/base.rb
@@ -6,7 +6,7 @@ require "erb"
require "active_support/core_ext/string/filters"
require "active_support/core_ext/string/inflections"
-require_relative "actions"
+require "rails/command/actions"
module Rails
module Command
diff --git a/railties/lib/rails/command/behavior.rb b/railties/lib/rails/command/behavior.rb
index 7a6dd28e1a..718e2d9ab2 100644
--- a/railties/lib/rails/command/behavior.rb
+++ b/railties/lib/rails/command/behavior.rb
@@ -19,46 +19,6 @@ module Rails
end
private
-
- # This code is based directly on the Text gem implementation.
- # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher.
- #
- # Returns a value representing the "cost" of transforming str1 into str2.
- def levenshtein_distance(str1, str2) # :doc:
- s = str1
- t = str2
- n = s.length
- m = t.length
-
- return m if (0 == n)
- return n if (0 == m)
-
- d = (0..m).to_a
- x = nil
-
- # avoid duplicating an enumerable object in the loop
- str2_codepoint_enumerable = str2.each_codepoint
-
- str1.each_codepoint.with_index do |char1, i|
- e = i + 1
-
- str2_codepoint_enumerable.with_index do |char2, j|
- cost = (char1 == char2) ? 0 : 1
- x = [
- d[j + 1] + 1, # insertion
- e + 1, # deletion
- d[j] + cost # substitution
- ].min
- d[j] = e
- e = x
- end
-
- d[m] = x
- end
-
- x
- end
-
# Prints a list of generators.
def print_list(base, namespaces)
return if namespaces.empty?
diff --git a/railties/lib/rails/command/helpers/editor.rb b/railties/lib/rails/command/helpers/editor.rb
new file mode 100644
index 0000000000..6191d97672
--- /dev/null
+++ b/railties/lib/rails/command/helpers/editor.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "active_support/encrypted_file"
+
+module Rails
+ module Command
+ module Helpers
+ module Editor
+ private
+ def ensure_editor_available(command:)
+ if ENV["EDITOR"].to_s.empty?
+ say "No $EDITOR to open file in. Assign one like this:"
+ say ""
+ say %(EDITOR="mate --wait" #{command})
+ say ""
+ say "For editors that fork and exit immediately, it's important to pass a wait flag,"
+ say "otherwise the credentials will be saved immediately with no chance to edit."
+
+ false
+ else
+ true
+ end
+ end
+
+ def catch_editing_exceptions
+ yield
+ rescue Interrupt
+ say "Aborted changing file: nothing saved."
+ rescue ActiveSupport::EncryptedFile::MissingKeyError => error
+ say error.message
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/command/spellchecker.rb b/railties/lib/rails/command/spellchecker.rb
new file mode 100644
index 0000000000..154358cd45
--- /dev/null
+++ b/railties/lib/rails/command/spellchecker.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Rails
+ module Command
+ module Spellchecker # :nodoc:
+ def self.suggest(word, from:)
+ DidYouMean::SpellChecker.new(dictionary: from.map(&:to_s)).correct(word).first
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb
index 1aea1e1a96..77961a0292 100644
--- a/railties/lib/rails/commands.rb
+++ b/railties/lib/rails/commands.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "command"
+require "rails/command"
aliases = {
"g" => "generate",
diff --git a/railties/lib/rails/commands/application/application_command.rb b/railties/lib/rails/commands/application/application_command.rb
index 13d47a63bc..f77553b830 100644
--- a/railties/lib/rails/commands/application/application_command.rb
+++ b/railties/lib/rails/commands/application/application_command.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../../generators"
-require_relative "../../generators/rails/app/app_generator"
+require "rails/generators"
+require "rails/generators/rails/app/app_generator"
module Rails
module Generators
diff --git a/railties/lib/rails/commands/console/console_command.rb b/railties/lib/rails/commands/console/console_command.rb
index 5dc695c240..e35faa5b01 100644
--- a/railties/lib/rails/commands/console/console_command.rb
+++ b/railties/lib/rails/commands/console/console_command.rb
@@ -3,7 +3,7 @@
require "irb"
require "irb/completion"
-require_relative "../../command/environment_argument"
+require "rails/command/environment_argument"
module Rails
class Console
diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb
index 88fb032d84..fa54c0362a 100644
--- a/railties/lib/rails/commands/credentials/credentials_command.rb
+++ b/railties/lib/rails/commands/credentials/credentials_command.rb
@@ -1,10 +1,13 @@
# frozen_string_literal: true
require "active_support"
+require "rails/command/helpers/editor"
module Rails
module Command
class CredentialsCommand < Rails::Command::Base # :nodoc:
+ include Helpers::Editor
+
no_commands do
def help
say "Usage:\n #{self.class.banner}"
@@ -16,43 +19,27 @@ module Rails
def edit
require_application_and_environment!
- ensure_editor_available || (return)
- ensure_master_key_has_been_added
+ ensure_editor_available(command: "bin/rails credentials:edit") || (return)
+ ensure_master_key_has_been_added if Rails.application.credentials.key.nil?
ensure_credentials_have_been_added
- change_credentials_in_system_editor
+ catch_editing_exceptions do
+ change_credentials_in_system_editor
+ end
say "New credentials encrypted and saved."
- rescue Interrupt
- say "Aborted changing credentials: nothing saved."
- rescue ActiveSupport::EncryptedFile::MissingKeyError => error
- say error.message
end
def show
require_application_and_environment!
- say Rails.application.credentials.read.presence ||
- "No credentials have been added yet. Use bin/rails credentials:edit to change that."
+
+ say Rails.application.credentials.read.presence || missing_credentials_message
end
private
- def ensure_editor_available
- if ENV["EDITOR"].to_s.empty?
- say "No $EDITOR to open credentials in. Assign one like this:"
- say ""
- say %(EDITOR="mate --wait" bin/rails credentials:edit)
- say ""
- say "For editors that fork and exit immediately, it's important to pass a wait flag,"
- say "otherwise the credentials will be saved immediately with no chance to edit."
-
- false
- else
- true
- end
- end
-
def ensure_master_key_has_been_added
master_key_generator.add_master_key_file
+ master_key_generator.ignore_master_key_file
end
def ensure_credentials_have_been_added
@@ -67,18 +54,26 @@ module Rails
def master_key_generator
- require_relative "../../generators"
- require_relative "../../generators/rails/master_key/master_key_generator"
+ require "rails/generators"
+ require "rails/generators/rails/master_key/master_key_generator"
Rails::Generators::MasterKeyGenerator.new
end
def credentials_generator
- require_relative "../../generators"
- require_relative "../../generators/rails/credentials/credentials_generator"
+ require "rails/generators"
+ require "rails/generators/rails/credentials/credentials_generator"
Rails::Generators::CredentialsGenerator.new
end
+
+ def missing_credentials_message
+ if Rails.application.credentials.key.nil?
+ "Missing master key to decrypt credentials. See bin/rails credentials:help"
+ else
+ "No credentials have been added yet. Use bin/rails credentials:edit to change that."
+ end
+ end
end
end
end
diff --git a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
index 5234969743..806b7de6d6 100644
--- a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
+++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../command/environment_argument"
+require "rails/command/environment_argument"
module Rails
class DBConsole
@@ -97,7 +97,7 @@ module Rails
elsif configurations[environment].blank? && configurations[connection].blank?
raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}"
else
- configurations[environment].presence || configurations[connection]
+ configurations[connection] || configurations[environment].presence
end
end
end
diff --git a/railties/lib/rails/commands/destroy/destroy_command.rb b/railties/lib/rails/commands/destroy/destroy_command.rb
index 686193ddb9..dd432d28fd 100644
--- a/railties/lib/rails/commands/destroy/destroy_command.rb
+++ b/railties/lib/rails/commands/destroy/destroy_command.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../generators"
+require "rails/generators"
module Rails
module Command
diff --git a/railties/lib/rails/commands/encrypted/encrypted_command.rb b/railties/lib/rails/commands/encrypted/encrypted_command.rb
new file mode 100644
index 0000000000..3bc8f76ce4
--- /dev/null
+++ b/railties/lib/rails/commands/encrypted/encrypted_command.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require "pathname"
+require "active_support"
+require "rails/command/helpers/editor"
+
+module Rails
+ module Command
+ class EncryptedCommand < Rails::Command::Base # :nodoc:
+ include Helpers::Editor
+
+ class_option :key, aliases: "-k", type: :string,
+ default: "config/master.key", desc: "The Rails.root relative path to the encryption key"
+
+ no_commands do
+ def help
+ say "Usage:\n #{self.class.banner}"
+ say ""
+ end
+ end
+
+ def edit(file_path)
+ require_application_and_environment!
+ encrypted = Rails.application.encrypted(file_path, key_path: options[:key])
+
+ ensure_editor_available(command: "bin/rails encrypted:edit") || (return)
+ ensure_encryption_key_has_been_added(options[:key]) if encrypted.key.nil?
+ ensure_encrypted_file_has_been_added(file_path, options[:key])
+
+ catch_editing_exceptions do
+ change_encrypted_file_in_system_editor(file_path, options[:key])
+ end
+
+ say "File encrypted and saved."
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
+ say "Couldn't decrypt #{file_path}. Perhaps you passed the wrong key?"
+ end
+
+ def show(file_path)
+ require_application_and_environment!
+ encrypted = Rails.application.encrypted(file_path, key_path: options[:key])
+
+ say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: options[:key], file_path: file_path)
+ end
+
+ private
+ def ensure_encryption_key_has_been_added(key_path)
+ encryption_key_file_generator.add_key_file(key_path)
+ encryption_key_file_generator.ignore_key_file(key_path)
+ end
+
+ def ensure_encrypted_file_has_been_added(file_path, key_path)
+ encrypted_file_generator.add_encrypted_file_silently(file_path, key_path)
+ end
+
+ def change_encrypted_file_in_system_editor(file_path, key_path)
+ Rails.application.encrypted(file_path, key_path: key_path).change do |tmp_path|
+ system("#{ENV["EDITOR"]} #{tmp_path}")
+ end
+ end
+
+
+ def encryption_key_file_generator
+ require "rails/generators"
+ require "rails/generators/rails/encryption_key_file/encryption_key_file_generator"
+
+ Rails::Generators::EncryptionKeyFileGenerator.new
+ end
+
+ def encrypted_file_generator
+ require "rails/generators"
+ require "rails/generators/rails/encrypted_file/encrypted_file_generator"
+
+ Rails::Generators::EncryptedFileGenerator.new
+ end
+
+ def missing_encrypted_message(key:, key_path:, file_path:)
+ if key.nil?
+ "Missing '#{key_path}' to decrypt data. See bin/rails encrypted:help"
+ else
+ "File '#{file_path}' does not exist. Use bin/rails encrypted:edit #{file_path} to change that."
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/generate/generate_command.rb b/railties/lib/rails/commands/generate/generate_command.rb
index 73f627637d..93d7a0ce3a 100644
--- a/railties/lib/rails/commands/generate/generate_command.rb
+++ b/railties/lib/rails/commands/generate/generate_command.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../generators"
+require "rails/generators"
module Rails
module Command
diff --git a/railties/lib/rails/commands/plugin/plugin_command.rb b/railties/lib/rails/commands/plugin/plugin_command.rb
index 5d3dfadf84..2b192abf9b 100644
--- a/railties/lib/rails/commands/plugin/plugin_command.rb
+++ b/railties/lib/rails/commands/plugin/plugin_command.rb
@@ -36,8 +36,8 @@ module Rails
private
def run_plugin_generator(plugin_args)
- require_relative "../../generators"
- require_relative "../../generators/rails/plugin/plugin_generator"
+ require "rails/generators"
+ require "rails/generators/rails/plugin/plugin_generator"
Rails::Generators::PluginGenerator.start plugin_args
end
end
diff --git a/railties/lib/rails/commands/routes/routes_command.rb b/railties/lib/rails/commands/routes/routes_command.rb
new file mode 100644
index 0000000000..b592a5212f
--- /dev/null
+++ b/railties/lib/rails/commands/routes/routes_command.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "rails/command"
+
+module Rails
+ module Command
+ class RoutesCommand < Base # :nodoc:
+ class_option :controller, aliases: "-c", desc: "Filter by a specific controller, e.g. PostsController or Admin::PostsController."
+ class_option :grep, aliases: "-g", desc: "Grep routes by a specific pattern."
+ class_option :expanded, type: :boolean, aliases: "-E", desc: "Print routes expanded vertically with parts explained."
+
+ def perform(*)
+ require_application_and_environment!
+ require "action_dispatch/routing/inspector"
+
+ say inspector.format(formatter, routes_filter)
+ end
+
+ private
+ def inspector
+ ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes)
+ end
+
+ def formatter
+ if options.key?("expanded")
+ ActionDispatch::Routing::ConsoleFormatter::Expanded.new
+ else
+ ActionDispatch::Routing::ConsoleFormatter::Sheet.new
+ end
+ end
+
+ def routes_filter
+ options.symbolize_keys.slice(:controller, :grep)
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/runner/runner_command.rb b/railties/lib/rails/commands/runner/runner_command.rb
index cd9462e08f..30fbf04982 100644
--- a/railties/lib/rails/commands/runner/runner_command.rb
+++ b/railties/lib/rails/commands/runner/runner_command.rb
@@ -32,13 +32,13 @@ module Rails
ARGV.replace(command_argv)
if code_or_file == "-"
- eval($stdin.read, binding, "stdin")
+ eval($stdin.read, TOPLEVEL_BINDING, "stdin")
elsif File.exist?(code_or_file)
$0 = code_or_file
Kernel.load code_or_file
else
begin
- eval(code_or_file, binding, __FILE__, __LINE__)
+ eval(code_or_file, TOPLEVEL_BINDING, __FILE__, __LINE__)
rescue SyntaxError, NameError => error
$stderr.puts "Please specify a valid ruby command or the path of a script to run."
$stderr.puts "Run '#{self.class.executable} -h' for help."
diff --git a/railties/lib/rails/commands/secrets/secrets_command.rb b/railties/lib/rails/commands/secrets/secrets_command.rb
index d93c4de74e..a36ccf314c 100644
--- a/railties/lib/rails/commands/secrets/secrets_command.rb
+++ b/railties/lib/rails/commands/secrets/secrets_command.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "active_support"
-require_relative "../../secrets"
+require "rails/secrets"
module Rails
module Command
@@ -15,7 +15,7 @@ module Rails
end
def setup
- generator.start
+ deprecate_in_favor_of_credentials_and_exit
end
def edit
@@ -42,11 +42,10 @@ module Rails
rescue Rails::Secrets::MissingKeyError => error
say error.message
rescue Errno::ENOENT => error
- raise unless error.message =~ /secrets\.yml\.enc/
-
- Rails::Secrets.read_template_for_editing do |tmp_path|
- system("#{ENV["EDITOR"]} #{tmp_path}")
- generator.skip_secrets_file { setup }
+ if error.message =~ /secrets\.yml\.enc/
+ deprecate_in_favor_of_credentials_and_exit
+ else
+ raise
end
end
@@ -55,11 +54,11 @@ module Rails
end
private
- def generator
- require_relative "../../generators"
- require_relative "../../generators/rails/encrypted_secrets/encrypted_secrets_generator"
+ def deprecate_in_favor_of_credentials_and_exit
+ say "Encrypted secrets is deprecated in favor of credentials. Run:"
+ say "bin/rails credentials:help"
- Rails::Generators::EncryptedSecretsGenerator
+ exit 1
end
end
end
diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb
index 785265d766..77b6c1f65d 100644
--- a/railties/lib/rails/commands/server/server_command.rb
+++ b/railties/lib/rails/commands/server/server_command.rb
@@ -6,7 +6,7 @@ require "action_dispatch"
require "rails"
require "active_support/deprecation"
require "active_support/core_ext/string/filters"
-require_relative "../../dev_caching"
+require "rails/dev_caching"
module Rails
class Server < ::Rack::Server
@@ -27,7 +27,7 @@ module Rails
app = super
if app.is_a?(Class)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Use `Rails::Application` subclass to start the server is deprecated and will be removed in Rails 6.0.
+ Using `Rails::Application` subclass to start the server is deprecated and will be removed in Rails 6.0.
Please change `run #{app}` to `run Rails.application` in config.ru.
MSG
end
@@ -43,18 +43,22 @@ module Rails
ENV["RAILS_ENV"] ||= options[:environment]
end
- def start
- print_boot_information
+ def start(after_stop_callback = nil)
trap(:INT) { exit }
create_tmp_directories
setup_dev_caching
log_to_stdout if options[:log_stdout]
- super
+ super()
ensure
- # The '-h' option calls exit before @options is set.
- # If we call 'options' with it unset, we get double help banners.
- puts "Exiting" unless @options && options[:daemonize]
+ after_stop_callback.call if after_stop_callback
+ end
+
+ def serveable? # :nodoc:
+ server
+ true
+ rescue LoadError, NameError
+ false
end
def middleware
@@ -65,6 +69,10 @@ module Rails
super.merge(@default_options)
end
+ def served_url
+ "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" unless use_puma?
+ end
+
private
def setup_dev_caching
if options[:environment] == "development"
@@ -72,13 +80,6 @@ module Rails
end
end
- def print_boot_information
- url = "on #{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" unless use_puma?
- puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
- puts "=> Rails #{Rails.version} application starting in #{Rails.env} #{url}"
- puts "=> Run `rails server -h` for more startup options"
- end
-
def create_tmp_directories
%w(cache pids sockets).each do |dir_to_make|
FileUtils.mkdir_p(File.join(Rails.root, "tmp", dir_to_make))
@@ -108,9 +109,15 @@ module Rails
module Command
class ServerCommand < Base # :nodoc:
+ # Hard-coding a bunch of handlers here as we don't have a public way of
+ # querying them from the Rack::Handler registry.
+ RACK_SERVERS = %w(cgi fastcgi webrick lsws scgi thin puma unicorn)
+
DEFAULT_PORT = 3000
DEFAULT_PID_PATH = "tmp/pids/server.pid".freeze
+ argument :using, optional: true
+
class_option :port, aliases: "-p", type: :numeric,
desc: "Runs Rails on the specified port - defaults to 3000.", banner: :port
class_option :binding, aliases: "-b", type: :string,
@@ -122,28 +129,39 @@ module Rails
desc: "Runs server as a Daemon."
class_option :environment, aliases: "-e", type: :string,
desc: "Specifies the environment to run this server under (development/test/production).", banner: :name
+ class_option :using, aliases: "-u", type: :string,
+ desc: "Specifies the Rack server used to run the application (thin/puma/webrick).", banner: :name
class_option :pid, aliases: "-P", type: :string, default: DEFAULT_PID_PATH,
desc: "Specifies the PID file."
class_option "dev-caching", aliases: "-C", type: :boolean, default: nil,
desc: "Specifies whether to perform caching in development."
class_option "restart", type: :boolean, default: nil, hide: true
+ class_option "early_hints", type: :boolean, default: nil, desc: "Enables HTTP/2 early hints."
def initialize(args = [], local_options = {}, config = {})
@original_options = local_options
super
- @server = self.args.shift
+ @using = deprecated_positional_rack_server(using) || options[:using]
@log_stdout = options[:daemon].blank? && (options[:environment] || Rails.env) == "development"
end
def perform
set_application_directory!
prepare_restart
+
Rails::Server.new(server_options).tap do |server|
# Require application after server sets environment to propagate
# the --environment option.
require APP_PATH
Dir.chdir(Rails.application.root)
- server.start
+
+ if server.serveable?
+ print_boot_information(server.server, server.served_url)
+ after_stop_callback = -> { say "Exiting" unless options[:daemon] }
+ server.start(after_stop_callback)
+ else
+ say rack_server_suggestion(using)
+ end
end
end
@@ -151,7 +169,7 @@ module Rails
def server_options
{
user_supplied_options: user_supplied_options,
- server: @server,
+ server: using,
log_stdout: @log_stdout,
Port: port,
Host: host,
@@ -161,7 +179,8 @@ module Rails
daemonize: options[:daemon],
pid: pid,
caching: options["dev-caching"],
- restart_cmd: restart_command
+ restart_cmd: restart_command,
+ early_hints: early_hints
}
end
end
@@ -200,7 +219,7 @@ module Rails
user_supplied_options << name
end
end
- user_supplied_options << :Host if ENV["HOST"]
+ user_supplied_options << :Host if ENV["HOST"] || ENV["BINDING"]
user_supplied_options << :Port if ENV["PORT"]
user_supplied_options.uniq
end
@@ -215,7 +234,17 @@ module Rails
options[:binding]
else
default_host = environment == "development" ? "localhost" : "0.0.0.0"
- ENV.fetch("HOST", default_host)
+
+ if ENV["HOST"] && !ENV["BINDING"]
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Using the `HOST` environment to specify the IP is deprecated and will be removed in Rails 6.1.
+ Please use `BINDING` environment instead.
+ MSG
+
+ return ENV["HOST"]
+ end
+
+ ENV.fetch("BINDING", default_host)
end
end
@@ -224,7 +253,11 @@ module Rails
end
def restart_command
- "bin/rails server #{@server} #{@original_options.join(" ")} --restart"
+ "bin/rails server #{using} #{@original_options.join(" ")} --restart"
+ end
+
+ def early_hints
+ options[:early_hints]
end
def pid
@@ -232,12 +265,50 @@ module Rails
end
def self.banner(*)
- "rails server [puma, thin etc] [options]"
+ "rails server [thin/puma/webrick] [options]"
end
def prepare_restart
FileUtils.rm_f(options[:pid]) if options[:restart]
end
+
+ def deprecated_positional_rack_server(value)
+ if value
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing the Rack server name as a regular argument is deprecated
+ and will be removed in the next Rails version. Please, use the -u
+ option instead.
+ MSG
+ value
+ end
+ end
+
+ def rack_server_suggestion(server)
+ if server.in?(RACK_SERVERS)
+ <<~MSG
+ Could not load server "#{server}". Maybe you need to the add it to the Gemfile?
+
+ gem "#{server}"
+
+ Run `rails server --help` for more options.
+ MSG
+ else
+ suggestions = Rails::Command::Spellchecker.suggest(server, from: RACK_SERVERS)
+
+ <<~MSG
+ Could not find server "#{server}". Maybe you meant #{suggestions.inspect}?
+ Run `rails server --help` for more options.
+ MSG
+ end
+ end
+
+ def print_boot_information(server, url)
+ say <<~MSG
+ => Booting #{ActiveSupport::Inflector.demodulize(server)}
+ => Rails #{Rails.version} application starting in #{Rails.env} #{url}
+ => Run `rails server --help` for more startup options
+ MSG
+ end
end
end
end
diff --git a/railties/lib/rails/commands/test/test_command.rb b/railties/lib/rails/commands/test/test_command.rb
index a2216553ca..00ea9ac4a6 100644
--- a/railties/lib/rails/commands/test/test_command.rb
+++ b/railties/lib/rails/commands/test/test_command.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "../../command"
-require_relative "../../test_unit/runner"
-require_relative "../../test_unit/reporter"
+require "rails/command"
+require "rails/test_unit/runner"
+require "rails/test_unit/reporter"
module Rails
module Command
diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb
index 70815d114d..d3a54d9364 100644
--- a/railties/lib/rails/configuration.rb
+++ b/railties/lib/rails/configuration.rb
@@ -2,8 +2,8 @@
require "active_support/ordered_options"
require "active_support/core_ext/object"
-require_relative "paths"
-require_relative "rack"
+require "rails/paths"
+require "rails/rack"
module Rails
module Configuration
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index cc2030d37d..6a13a84108 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "railtie"
-require_relative "engine/railties"
+require "rails/railtie"
+require "rails/engine/railties"
require "active_support/core_ext/module/delegation"
require "pathname"
require "thread"
@@ -439,8 +439,8 @@ module Rails
# Load console and invoke the registered hooks.
# Check <tt>Rails::Railtie.console</tt> for more info.
def load_console(app = self)
- require_relative "console/app"
- require_relative "console/helpers"
+ require "rails/console/app"
+ require "rails/console/helpers"
run_console_blocks(app)
self
end
@@ -463,7 +463,7 @@ module Rails
# Load Rails generators and invoke the registered hooks.
# Check <tt>Rails::Railtie.generators</tt> for more info.
def load_generators(app = self)
- require_relative "generators"
+ require "rails/generators"
run_generators_blocks(app)
Rails::Generators.configure!(app.config.generators)
self
diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb
index 3854907507..05218640c6 100644
--- a/railties/lib/rails/engine/commands.rb
+++ b/railties/lib/rails/engine/commands.rb
@@ -6,4 +6,4 @@ unless defined?(APP_PATH)
end
end
-require_relative "../commands"
+require "rails/commands"
diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb
index 16ba7f9eb8..6bf0406b21 100644
--- a/railties/lib/rails/engine/configuration.rb
+++ b/railties/lib/rails/engine/configuration.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../railtie/configuration"
+require "rails/railtie/configuration"
module Rails
class Engine
diff --git a/railties/lib/rails/engine/updater.rb b/railties/lib/rails/engine/updater.rb
index 21a0fc5562..be7a47124a 100644
--- a/railties/lib/rails/engine/updater.rb
+++ b/railties/lib/rails/engine/updater.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../generators"
-require_relative "../generators/rails/plugin/plugin_generator"
+require "rails/generators"
+require "rails/generators/rails/plugin/plugin_generator"
module Rails
class Engine
diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb
index 92b5e0392a..54bfbdd516 100644
--- a/railties/lib/rails/gem_version.rb
+++ b/railties/lib/rails/gem_version.rb
@@ -7,8 +7,8 @@ module Rails
end
module VERSION
- MAJOR = 5
- MINOR = 2
+ MAJOR = 6
+ MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb
index a630d55e59..f8460bd4ee 100644
--- a/railties/lib/rails/generators.rb
+++ b/railties/lib/rails/generators.rb
@@ -4,7 +4,7 @@ activesupport_path = File.expand_path("../../../activesupport/lib", __dir__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
require "thor/group"
-require_relative "command"
+require "rails/command"
require "active_support"
require "active_support/core_ext/object/blank"
@@ -218,6 +218,10 @@ module Rails
rails.delete("app")
rails.delete("plugin")
rails.delete("encrypted_secrets")
+ rails.delete("encrypted_file")
+ rails.delete("encryption_key_file")
+ rails.delete("master_key")
+ rails.delete("credentials")
hidden_namespaces.each { |n| groups.delete(n.to_s) }
@@ -272,11 +276,11 @@ module Rails
klass.start(args, config)
else
options = sorted_groups.flat_map(&:last)
- suggestions = options.sort_by { |suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3)
- msg = "Could not find generator '#{namespace}'. ".dup
- msg << "Maybe you meant #{ suggestions.map { |s| "'#{s}'" }.to_sentence(last_word_connector: " or ", locale: :en) }\n"
- msg << "Run `rails generate --help` for more options."
- puts msg
+ suggestion = Rails::Command::Spellchecker.suggest(namespace.to_s, from: options)
+ puts <<~MSG
+ Could not find generator '#{namespace}'. Maybe you meant #{suggestion.inspect}?
+ Run `rails generate --help` for more options.
+ MSG
end
end
diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb
index 9800e5750a..d85bbfb03e 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/string/strip"
+
module Rails
module Generators
module Actions
@@ -221,6 +223,7 @@ module Rails
# rake("db:migrate")
# rake("db:migrate", env: "production")
# rake("gems:install", sudo: true)
+ # rake("gems:install", capture: true)
def rake(command, options = {})
execute_command :rake, command, options
end
@@ -230,6 +233,7 @@ module Rails
# rails_command("db:migrate")
# rails_command("db:migrate", env: "production")
# rails_command("gems:install", sudo: true)
+ # rails_command("gems:install", capture: true)
def rails_command(command, options = {})
execute_command :rails, command, options
end
@@ -292,7 +296,11 @@ module Rails
log executor, command
env = options[:env] || ENV["RAILS_ENV"] || "development"
sudo = options[:sudo] && !Gem.win_platform? ? "sudo " : ""
- in_root { run("#{sudo}#{extify(executor)} #{command} RAILS_ENV=#{env}", verbose: false) }
+ config = { verbose: false }
+
+ config.merge!(capture: options[:capture]) if options[:capture]
+
+ in_root { run("#{sudo}#{extify(executor)} #{command} RAILS_ENV=#{env}", config) }
end
# Add an extension to the given name based on the platform.
diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb
index c8688fb7f3..f51542f3ec 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -2,11 +2,10 @@
require "fileutils"
require "digest/md5"
-require "active_support/core_ext/string/strip"
-require_relative "../version" unless defined?(Rails::VERSION)
+require "rails/version" unless defined?(Rails::VERSION)
require "open-uri"
require "uri"
-require_relative "../generators"
+require "rails/generators"
require "active_support/core_ext/array/extract_options"
module Rails
@@ -26,75 +25,81 @@ module Rails
end
def self.add_shared_options_for(name)
- class_option :template, type: :string, aliases: "-m",
- desc: "Path to some #{name} template (can be a filesystem path or URL)"
+ class_option :template, type: :string, aliases: "-m",
+ desc: "Path to some #{name} template (can be a filesystem path or URL)"
- class_option :database, type: :string, aliases: "-d", default: "sqlite3",
- desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})"
+ class_option :database, type: :string, aliases: "-d", default: "sqlite3",
+ desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})"
- class_option :skip_yarn, type: :boolean, default: false,
- desc: "Don't use Yarn for managing JavaScript dependencies"
+ class_option :skip_yarn, type: :boolean, default: false,
+ desc: "Don't use Yarn for managing JavaScript dependencies"
- class_option :skip_gemfile, type: :boolean, default: false,
- desc: "Don't create a Gemfile"
+ class_option :skip_gemfile, type: :boolean, default: false,
+ desc: "Don't create a Gemfile"
- class_option :skip_git, type: :boolean, aliases: "-G", default: false,
- desc: "Skip .gitignore file"
+ class_option :skip_git, type: :boolean, aliases: "-G", default: false,
+ desc: "Skip .gitignore file"
- class_option :skip_keeps, type: :boolean, default: false,
- desc: "Skip source control .keep files"
+ class_option :skip_keeps, type: :boolean, default: false,
+ desc: "Skip source control .keep files"
- class_option :skip_action_mailer, type: :boolean, aliases: "-M",
- default: false,
- desc: "Skip Action Mailer files"
+ class_option :skip_action_mailer, type: :boolean, aliases: "-M",
+ default: false,
+ desc: "Skip Action Mailer files"
- class_option :skip_active_record, type: :boolean, aliases: "-O", default: false,
- desc: "Skip Active Record files"
+ class_option :skip_active_record, type: :boolean, aliases: "-O", default: false,
+ desc: "Skip Active Record files"
- class_option :skip_puma, type: :boolean, aliases: "-P", default: false,
- desc: "Skip Puma related files"
+ class_option :skip_active_storage, type: :boolean, default: false,
+ desc: "Skip Active Storage files"
- class_option :skip_action_cable, type: :boolean, aliases: "-C", default: false,
- desc: "Skip Action Cable files"
+ class_option :skip_puma, type: :boolean, aliases: "-P", default: false,
+ desc: "Skip Puma related files"
- class_option :skip_sprockets, type: :boolean, aliases: "-S", default: false,
- desc: "Skip Sprockets files"
+ class_option :skip_action_cable, type: :boolean, aliases: "-C", default: false,
+ desc: "Skip Action Cable files"
- class_option :skip_spring, type: :boolean, default: false,
- desc: "Don't install Spring application preloader"
+ class_option :skip_sprockets, type: :boolean, aliases: "-S", default: false,
+ desc: "Skip Sprockets files"
- class_option :skip_listen, type: :boolean, default: false,
- desc: "Don't generate configuration that depends on the listen gem"
+ class_option :skip_spring, type: :boolean, default: false,
+ desc: "Don't install Spring application preloader"
- class_option :skip_coffee, type: :boolean, default: false,
- desc: "Don't use CoffeeScript"
+ 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"
+ class_option :skip_coffee, type: :boolean, default: false,
+ desc: "Don't use CoffeeScript"
- class_option :skip_turbolinks, type: :boolean, default: false,
- desc: "Skip turbolinks gem"
+ class_option :skip_javascript, type: :boolean, aliases: "-J", default: false,
+ desc: "Skip JavaScript files"
- class_option :skip_test, type: :boolean, aliases: "-T", default: false,
- desc: "Skip test files"
+ class_option :skip_turbolinks, type: :boolean, default: false,
+ desc: "Skip turbolinks gem"
- class_option :skip_system_test, type: :boolean, default: false,
- desc: "Skip system test files"
+ class_option :skip_test, type: :boolean, aliases: "-T", default: false,
+ desc: "Skip test files"
- class_option :dev, type: :boolean, default: false,
- desc: "Setup the #{name} with Gemfile pointing to your Rails checkout"
+ class_option :skip_system_test, type: :boolean, default: false,
+ desc: "Skip system test files"
- class_option :edge, type: :boolean, default: false,
- desc: "Setup the #{name} with Gemfile pointing to Rails repository"
+ class_option :skip_bootsnap, type: :boolean, default: false,
+ desc: "Skip bootsnap gem"
- class_option :rc, type: :string, default: nil,
- desc: "Path to file containing extra configuration options for rails command"
+ class_option :dev, type: :boolean, default: false,
+ desc: "Setup the #{name} with Gemfile pointing to your Rails checkout"
- class_option :no_rc, type: :boolean, default: false,
- desc: "Skip loading of extra configuration options from .railsrc file"
+ class_option :edge, type: :boolean, default: false,
+ desc: "Setup the #{name} with Gemfile pointing to Rails repository"
- class_option :help, type: :boolean, aliases: "-h", group: :rails,
- desc: "Show this help message and quit"
+ class_option :rc, type: :string, default: nil,
+ desc: "Path to file containing extra configuration options for rails command"
+
+ class_option :no_rc, type: :boolean, default: false,
+ desc: "Skip loading of extra configuration options from .railsrc file"
+
+ class_option :help, type: :boolean, aliases: "-h", group: :rails,
+ desc: "Show this help message and quit"
end
def initialize(*args)
@@ -189,15 +194,33 @@ module Rails
def webserver_gemfile_entry # :doc:
return [] if options[:skip_puma]
comment = "Use Puma as the app server"
- GemfileEntry.new("puma", "~> 3.7", comment)
+ GemfileEntry.new("puma", "~> 3.11", comment)
end
def include_all_railties? # :doc:
- options.values_at(:skip_active_record, :skip_action_mailer, :skip_test, :skip_sprockets, :skip_action_cable).none?
+ [
+ options.values_at(
+ :skip_active_record,
+ :skip_action_mailer,
+ :skip_test,
+ :skip_sprockets,
+ :skip_action_cable
+ ),
+ skip_active_storage?
+ ].flatten.none?
end
def comment_if(value) # :doc:
- options[value] ? "# " : ""
+ question = "#{value}?"
+
+ comment =
+ if respond_to?(question, true)
+ send(question)
+ else
+ options[value]
+ end
+
+ comment ? "# " : ""
end
def keeps? # :doc:
@@ -208,6 +231,10 @@ module Rails
!options[:skip_active_record] && options[:database] == "sqlite3"
end
+ def skip_active_storage? # :doc:
+ options[:skip_active_storage] || options[:skip_active_record]
+ end
+
class GemfileEntry < Struct.new(:name, :version, :comment, :options, :commented_out)
def initialize(name, version, comment, options = {}, commented_out = false)
super
@@ -241,17 +268,14 @@ module Rails
end
def rails_gemfile_entry
- dev_edge_common = [
- GemfileEntry.github("arel", "rails/arel"),
- ]
if options.dev?
[
GemfileEntry.path("rails", Rails::Generators::RAILS_DEV_PATH)
- ] + dev_edge_common
+ ]
elsif options.edge?
[
GemfileEntry.github("rails", "rails/rails")
- ] + dev_edge_common
+ ]
else
[GemfileEntry.version("rails",
rails_version_specifier,
@@ -275,8 +299,8 @@ module Rails
def gem_for_database
# %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver jdbcmysql jdbcsqlite3 jdbcpostgresql )
case options[:database]
- when "mysql" then ["mysql2", [">= 0.3.18", "< 0.5"]]
- when "postgresql" then ["pg", ["~> 0.18"]]
+ when "mysql" then ["mysql2", [">= 0.4.4", "< 0.6.0"]]
+ when "postgresql" then ["pg", [">= 0.18", "< 2.0"]]
when "oracle" then ["activerecord-oracle_enhanced-adapter", nil]
when "frontbase" then ["ruby-frontbase", nil]
when "sqlserver" then ["activerecord-sqlserver-adapter", nil]
@@ -290,11 +314,13 @@ module Rails
def convert_database_option_for_jruby
if defined?(JRUBY_VERSION)
- case options[:database]
- when "postgresql" then options[:database].replace "jdbcpostgresql"
- when "mysql" then options[:database].replace "jdbcmysql"
- when "sqlite3" then options[:database].replace "jdbcsqlite3"
+ opt = options.dup
+ case opt[:database]
+ when "postgresql" then opt[:database] = "jdbcpostgresql"
+ when "mysql" then opt[:database] = "jdbcmysql"
+ when "sqlite3" then opt[:database] = "jdbcsqlite3"
end
+ self.options = opt.freeze
end
end
@@ -369,7 +395,7 @@ module Rails
return [] if options[:skip_action_cable]
comment = "Use Redis adapter to run Action Cable in production"
gems = []
- gems << GemfileEntry.new("redis", "~> 3.0", comment, {}, true)
+ gems << GemfileEntry.new("redis", "~> 4.0", comment, {}, true)
gems
end
@@ -413,6 +439,10 @@ module Rails
!options[:skip_listen] && os_supports_listen_out_of_the_box?
end
+ def depend_on_bootsnap?
+ !options[:skip_bootsnap] && !options[:dev] && !defined?(JRUBY_VERSION)
+ end
+
def os_supports_listen_out_of_the_box?
RbConfig::CONFIG["host_os"] =~ /darwin|linux/
end
diff --git a/railties/lib/rails/generators/css/assets/assets_generator.rb b/railties/lib/rails/generators/css/assets/assets_generator.rb
index 5f7be769b2..f657d1e50f 100644
--- a/railties/lib/rails/generators/css/assets/assets_generator.rb
+++ b/railties/lib/rails/generators/css/assets/assets_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../named_base"
+require "rails/generators/named_base"
module Css # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb
index 5996cb1483..89c560f382 100644
--- a/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb
@@ -1,17 +1,17 @@
# frozen_string_literal: true
-require_relative "../../named_base"
+require "rails/generators/named_base"
module Css # :nodoc:
module Generators # :nodoc:
class ScaffoldGenerator < Rails::Generators::NamedBase # :nodoc:
+ source_root Rails::Generators::ScaffoldGenerator.source_root
+
# In order to allow the Sass generators to pick up the default Rails CSS and
# transform it, we leave it in a standard location for the CSS stylesheet
# generators to handle. For the simple, default case, just copy it over.
def copy_stylesheet
- dir = Rails::Generators::ScaffoldGenerator.source_root
- file = File.join(dir, "scaffold.css")
- create_file "app/assets/stylesheets/scaffold.css", File.read(file)
+ copy_file "scaffold.css", "app/assets/stylesheets/scaffold.css"
end
end
end
diff --git a/railties/lib/rails/generators/erb.rb b/railties/lib/rails/generators/erb.rb
index 2c20834611..ba20bcd32a 100644
--- a/railties/lib/rails/generators/erb.rb
+++ b/railties/lib/rails/generators/erb.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "named_base"
+require "rails/generators/named_base"
module Erb # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/erb/controller/controller_generator.rb b/railties/lib/rails/generators/erb/controller/controller_generator.rb
index 1a6c84288b..8e13744b2a 100644
--- a/railties/lib/rails/generators/erb/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/erb/controller/controller_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../erb"
+require "rails/generators/erb"
module Erb # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/erb/controller/templates/view.html.erb b/railties/lib/rails/generators/erb/controller/templates/view.html.erb.tt
index cd54d13d83..cd54d13d83 100644
--- a/railties/lib/rails/generators/erb/controller/templates/view.html.erb
+++ b/railties/lib/rails/generators/erb/controller/templates/view.html.erb.tt
diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
index 5774d86c8e..997602cb8c 100644
--- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
+++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../erb"
+require "rails/generators/erb"
module Erb # :nodoc:
module Generators # :nodoc:
@@ -35,7 +35,7 @@ module Erb # :nodoc:
end
def file_name
- @_file_name ||= super.gsub(/_mailer/i, "")
+ @_file_name ||= super.sub(/_mailer\z/i, "")
end
end
end
diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.html.erb b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb.tt
index b5045671b3..b5045671b3 100644
--- a/railties/lib/rails/generators/erb/mailer/templates/view.html.erb
+++ b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb.tt
diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb.tt
index 342285df19..342285df19 100644
--- a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb
+++ b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb.tt
diff --git a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
index e80c6d4b7d..2fc04e4094 100644
--- a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../../erb"
-require_relative "../../resource_helpers"
+require "rails/generators/erb"
+require "rails/generators/resource_helpers"
module Erb # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt
index 4f2e84f924..518cb1121e 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt
@@ -1,4 +1,4 @@
-<%%= form_with(model: <%= singular_table_name %>, local: true) do |form| %>
+<%%= form_with(model: <%= model_resource_name %>, local: true) do |form| %>
<%% if <%= singular_table_name %>.errors.any? %>
<div id="error_explanation">
<h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
@@ -15,15 +15,15 @@
<div class="field">
<% if attribute.password_digest? -%>
<%%= form.label :password %>
- <%%= form.password_field :password, id: :<%= field_id(:password) %> %>
+ <%%= form.password_field :password %>
</div>
<div class="field">
<%%= form.label :password_confirmation %>
- <%%= form.password_field :password_confirmation, id: :<%= field_id(:password_confirmation) %> %>
+ <%%= form.password_field :password_confirmation %>
<% else -%>
<%%= form.label :<%= attribute.column_name %> %>
- <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, id: :<%= field_id(attribute.column_name) %> %>
+ <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %>
<% end -%>
</div>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb.tt
index 81329473d9..81329473d9 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb.tt
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt
index 5f4904fee1..e1ede7c713 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt
@@ -18,9 +18,9 @@
<% attributes.reject(&:password_digest?).each do |attribute| -%>
<td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td>
<% end -%>
- <td><%%= link_to 'Show', <%= singular_table_name %> %></td>
- <td><%%= link_to 'Edit', edit_<%= singular_table_name %>_path(<%= singular_table_name %>) %></td>
- <td><%%= link_to 'Destroy', <%= singular_table_name %>, method: :delete, data: { confirm: 'Are you sure?' } %></td>
+ <td><%%= link_to 'Show', <%= model_resource_name %> %></td>
+ <td><%%= link_to 'Edit', edit_<%= singular_route_name %>_path(<%= singular_table_name %>) %></td>
+ <td><%%= link_to 'Destroy', <%= model_resource_name %>, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<%% end %>
</tbody>
@@ -28,4 +28,4 @@
<br>
-<%%= link_to 'New <%= singular_table_name.titleize %>', new_<%= singular_table_name %>_path %>
+<%%= link_to 'New <%= singular_table_name.titleize %>', new_<%= singular_route_name %>_path %>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb.tt
index 9b2b2f4875..9b2b2f4875 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb.tt
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt
index 5e634153be..5e634153be 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt
diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb
index 2728459968..f7fd30a5fb 100644
--- a/railties/lib/rails/generators/generated_attribute.rb
+++ b/railties/lib/rails/generators/generated_attribute.rb
@@ -75,7 +75,7 @@ module Rails
when :date then :date_select
when :text then :text_area
when :boolean then :check_box
- else
+ else
:text_field
end
end
@@ -91,7 +91,7 @@ module Rails
when :text then "MyText"
when :boolean then false
when :references, :belongs_to then nil
- else
+ else
""
end
end
diff --git a/railties/lib/rails/generators/js/assets/assets_generator.rb b/railties/lib/rails/generators/js/assets/assets_generator.rb
index ea151aa04e..9d32c666dc 100644
--- a/railties/lib/rails/generators/js/assets/assets_generator.rb
+++ b/railties/lib/rails/generators/js/assets/assets_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../named_base"
+require "rails/generators/named_base"
module Js # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb
index 7162b8c0b4..5081060895 100644
--- a/railties/lib/rails/generators/migration.rb
+++ b/railties/lib/rails/generators/migration.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "active_support/concern"
-require_relative "actions/create_migration"
+require "rails/generators/actions/create_migration"
module Rails
module Generators
@@ -63,7 +63,12 @@ module Rails
numbered_destination = File.join(dir, ["%migration_number%", base].join("_"))
create_migration numbered_destination, nil, config do
- ERB.new(::File.binread(source), nil, "-", "@output_buffer").result(context)
+ match = ERB.version.match(/\Aerb\.rb \[(?<version>[^ ]+) /)
+ if match && match[:version] >= "2.2.0" # Ruby 2.6+
+ ERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer").result(context)
+ else
+ ERB.new(::File.binread(source), nil, "-", "@output_buffer").result(context)
+ end
end
end
end
diff --git a/railties/lib/rails/generators/model_helpers.rb b/railties/lib/rails/generators/model_helpers.rb
index aa3564476a..50078404b3 100644
--- a/railties/lib/rails/generators/model_helpers.rb
+++ b/railties/lib/rails/generators/model_helpers.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "active_model"
+require "rails/generators/active_model"
module Rails
module Generators
diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb
index fe8447be23..d6732f8ff1 100644
--- a/railties/lib/rails/generators/named_base.rb
+++ b/railties/lib/rails/generators/named_base.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
-require "active_support/core_ext/module/introspection"
-require_relative "base"
-require_relative "generated_attribute"
+require "rails/generators/base"
+require "rails/generators/generated_attribute"
module Rails
module Generators
@@ -32,12 +31,8 @@ module Rails
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
- attr_reader :file_name
-
private
+ attr_reader :file_name
# FIXME: We are avoiding to use alias because a bug on thor that make
# this method public and add it to the task list.
@@ -100,11 +95,11 @@ module Rails
end
def index_helper # :doc:
- uncountable? ? "#{plural_table_name}_index" : plural_table_name
+ uncountable? ? "#{plural_route_name}_index" : plural_route_name
end
def show_helper # :doc:
- "#{singular_table_name}_url(@#{singular_table_name})"
+ "#{singular_route_name}_url(@#{singular_table_name})"
end
def edit_helper # :doc:
@@ -112,11 +107,7 @@ module Rails
end
def new_helper # :doc:
- "new_#{singular_table_name}_url"
- end
-
- def field_id(attribute_name)
- [singular_table_name, attribute_name].join("_")
+ "new_#{singular_route_name}_url"
end
def singular_table_name # :doc:
@@ -152,6 +143,35 @@ module Rails
end
end
+ def redirect_resource_name # :doc:
+ model_resource_name(prefix: "@")
+ end
+
+ def model_resource_name(prefix: "") # :doc:
+ resource_name = "#{prefix}#{singular_table_name}"
+ if options[:model_name]
+ "[#{controller_class_path.map { |name| ":" + name }.join(", ")}, #{resource_name}]"
+ else
+ resource_name
+ end
+ end
+
+ def singular_route_name # :doc:
+ if options[:model_name]
+ "#{controller_class_path.join('_')}_#{singular_table_name}"
+ else
+ singular_table_name
+ end
+ end
+
+ def plural_route_name # :doc:
+ if options[:model_name]
+ "#{controller_class_path.join('_')}_#{plural_table_name}"
+ else
+ plural_table_name
+ end
+ end
+
def assign_names!(name)
@class_path = name.include?("/") ? name.split("/") : name.split("::")
@class_path.map!(&:underscore)
@@ -193,7 +213,7 @@ module Rails
#
def self.check_class_collision(options = {}) # :doc:
define_method :check_class_collision do
- name = if respond_to?(:controller_class_name) # for ScaffoldBase
+ name = if respond_to?(:controller_class_name) # for ResourceHelpers
controller_class_name
else
class_name
diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb
index ac82ff6633..395ac7ef2f 100644
--- a/railties/lib/rails/generators/rails/app/app_generator.rb
+++ b/railties/lib/rails/generators/rails/app/app_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../app_base"
+require "rails/generators/app_base"
module Rails
module ActionMethods # :nodoc:
@@ -69,7 +69,7 @@ module Rails
def version_control
if !options[:skip_git] && !options[:pretend]
- run "git init"
+ run "git init", capture: options[:quiet]
end
end
@@ -114,7 +114,7 @@ module Rails
template "cable.yml" unless options[:skip_action_cable]
template "puma.rb" unless options[:skip_puma]
template "spring.rb" if spring_install?
- template "storage.yml"
+ template "storage.yml" unless skip_active_storage?
directory "environments"
directory "initializers"
@@ -128,6 +128,9 @@ module Rails
active_storage_config_exist = File.exist?("config/storage.yml")
rack_cors_config_exist = File.exist?("config/initializers/cors.rb")
assets_config_exist = File.exist?("config/initializers/assets.rb")
+ csp_config_exist = File.exist?("config/initializers/content_security_policy.rb")
+
+ @config_target_version = Rails.application.config.loaded_config_version || "5.0"
config
@@ -139,7 +142,7 @@ module Rails
template "config/cable.yml"
end
- if !active_storage_config_exist
+ if !skip_active_storage? && !active_storage_config_exist
template "config/storage.yml"
end
@@ -155,27 +158,27 @@ module Rails
unless assets_config_exist
remove_file "config/initializers/assets.rb"
end
+
+ unless csp_config_exist
+ remove_file "config/initializers/content_security_policy.rb"
+ end
end
end
def master_key
- return if options[:pretend]
-
- require_relative "../master_key/master_key_generator"
+ return if options[:pretend] || options[:dummy_app]
- after_bundle do
- Rails::Generators::MasterKeyGenerator.new.add_master_key_file
- end
+ require "rails/generators/rails/master_key/master_key_generator"
+ master_key_generator = Rails::Generators::MasterKeyGenerator.new([], quiet: options[:quiet], force: options[:force])
+ master_key_generator.add_master_key_file_silently
+ master_key_generator.ignore_master_key_file_silently
end
def credentials
- return if options[:pretend]
-
- require_relative "../credentials/credentials_generator"
+ return if options[:pretend] || options[:dummy_app]
- after_bundle do
- Rails::Generators::CredentialsGenerator.new.add_credentials_file_silently
- end
+ require "rails/generators/rails/credentials/credentials_generator"
+ Rails::Generators::CredentialsGenerator.new([], quiet: options[:quiet]).add_credentials_file_silently
end
def database_yml
@@ -232,6 +235,10 @@ module Rails
def vendor
empty_directory_with_keep_file "vendor"
end
+
+ def config_target_version
+ defined?(@config_target_version) ? @config_target_version : Rails::VERSION::STRING.to_f
+ end
end
module Generators
@@ -241,7 +248,7 @@ module Rails
RESERVED_NAMES = %w[application destroy plugin runner test]
class AppGenerator < AppBase # :nodoc:
- WEBPACKS = %w( react vue angular elm )
+ WEBPACKS = %w( react vue angular elm stimulus )
add_shared_options_for "application"
@@ -347,6 +354,14 @@ module Rails
build(:public_directory)
end
+ def create_tmp_files
+ build(:tmp)
+ end
+
+ def create_vendor_files
+ build(:vendor)
+ end
+
def create_test_files
build(:test) unless options[:skip_test]
end
@@ -355,12 +370,8 @@ module Rails
build(:system_test) if depends_on_system_test?
end
- def create_tmp_files
- build(:tmp)
- end
-
- def create_vendor_files
- build(:vendor)
+ def create_storage_files
+ build(:storage) unless skip_active_storage?
end
def delete_app_assets_if_api_option
@@ -378,9 +389,13 @@ module Rails
end
end
- def delete_application_layout_file_if_api_option
+ def delete_app_views_if_api_option
if options[:api]
- remove_file "app/views/layouts/application.html.erb"
+ if options[:skip_action_mailer]
+ remove_dir "app/views"
+ else
+ remove_file "app/views/layouts/application.html.erb"
+ end
end
end
@@ -432,6 +447,7 @@ module Rails
def delete_non_api_initializers_if_api_option
if options[:api]
remove_file "config/initializers/cookies_serializer.rb"
+ remove_file "config/initializers/content_security_policy.rb"
end
end
@@ -443,7 +459,7 @@ module Rails
def delete_new_framework_defaults
unless options[:update]
- remove_file "config/initializers/new_framework_defaults_5_2.rb"
+ remove_file "config/initializers/new_framework_defaults_6_0.rb"
end
end
@@ -509,10 +525,6 @@ module Rails
end
end
- def app_secret
- SecureRandom.hex(64)
- end
-
def mysql_socket
@mysql_socket ||= [
"/tmp/mysql.sock", # default
@@ -559,7 +571,7 @@ module Rails
def handle_version_request!(argument)
if ["--version", "-v"].include?(argument)
- require_relative "../../../version"
+ require "rails/version"
puts "Rails #{Rails::VERSION::STRING}"
exit(0)
end
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
index 770247d8ca..1567333023 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
@@ -20,16 +20,20 @@ ruby <%= "'#{RUBY_VERSION}'" -%>
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'
+<% unless skip_active_storage? -%>
# Use ActiveStorage variant
-# gem 'mini_magick', '~> 4.8'
+# gem 'image_processing', '~> 1.2'
+<% end -%>
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development
+<% if depend_on_bootsnap? -%>
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.1.0', require: false
+<%- end -%>
<%- if options.api? -%>
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
# gem 'rack-cors'
@@ -39,11 +43,6 @@ gem 'bootsnap', '>= 1.1.0', require: false
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
- <%- if depends_on_system_test? -%>
- # Adds support for Capybara system testing and selenium driver
- gem 'capybara', '~> 2.13'
- gem 'selenium-webdriver'
- <%- end -%>
end
group :development do
@@ -66,6 +65,16 @@ group :development do
<% end -%>
<% end -%>
end
+
+<%- if depends_on_system_test? -%>
+group :test do
+ # Adds support for Capybara system testing and selenium driver
+ gem 'capybara', '>= 2.15'
+ gem 'selenium-webdriver'
+ # Easy installation and use of chromedriver to run system tests with Chrome
+ gem 'chromedriver-helper'
+end
+<%- end -%>
<% end -%>
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
diff --git a/railties/lib/rails/generators/rails/app/templates/README.md b/railties/lib/rails/generators/rails/app/templates/README.md.tt
index 7db80e4ca1..7db80e4ca1 100644
--- a/railties/lib/rails/generators/rails/app/templates/README.md
+++ b/railties/lib/rails/generators/rails/app/templates/README.md.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile.tt
index e85f913914..e85f913914 100644
--- a/railties/lib/rails/generators/rails/app/templates/Rakefile
+++ b/railties/lib/rails/generators/rails/app/templates/Rakefile.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt
index 62fd04f113..5183bcd256 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt
+++ b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt
@@ -12,7 +12,9 @@
//
<% unless options[:skip_javascript] -%>
//= require rails-ujs
+<% unless skip_active_storage? -%>
//= require activestorage
+<% end -%>
<% unless options[:skip_turbolinks] -%>
//= require turbolinks
<% end -%>
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.tt
index 739aa5f022..739aa5f022 100644
--- 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.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css.tt
index d05ea0f511..d05ea0f511 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css
+++ b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb.tt
index d672697283..d672697283 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb
+++ b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb.tt
index 0ff5442f47..0ff5442f47 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb
+++ b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb.tt
index de6be7945c..de6be7945c 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb
+++ b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt
index a009ace51c..a009ace51c 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb
+++ b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb b/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb.tt
index 286b2239d1..286b2239d1 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb
+++ b/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb b/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb.tt
index 10a4cba84d..10a4cba84d 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb
+++ b/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
index 5460155b3e..ef715f1368 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
@@ -3,6 +3,7 @@
<head>
<title><%= camelized %></title>
<%%= csrf_meta_tags %>
+ <%%= csp_meta_tag %>
<%- if options[:skip_javascript] -%>
<%%= stylesheet_link_tag 'application', media: 'all' %>
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/bundle b/railties/lib/rails/generators/rails/app/templates/bin/bundle.tt
index a84f0afe47..a84f0afe47 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/bundle
+++ b/railties/lib/rails/generators/rails/app/templates/bin/bundle.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rails b/railties/lib/rails/generators/rails/app/templates/bin/rails.tt
index 513a2e0183..513a2e0183 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/rails
+++ b/railties/lib/rails/generators/rails/app/templates/bin/rails.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rake b/railties/lib/rails/generators/rails/app/templates/bin/rake.tt
index d14fc8395b..d14fc8395b 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/rake
+++ b/railties/lib/rails/generators/rails/app/templates/bin/rake.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update.tt b/railties/lib/rails/generators/rails/app/templates/bin/update.tt
index d744bec32f..70cc71d83b 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/update.tt
+++ b/railties/lib/rails/generators/rails/app/templates/bin/update.tt
@@ -15,6 +15,11 @@ chdir APP_ROOT do
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')
+<% unless options.skip_yarn? -%>
+
+ # Install JavaScript dependencies if using Yarn
+ # system('bin/yarn')
+<% end -%>
<% unless options.skip_active_record? -%>
puts "\n== Updating database =="
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/yarn b/railties/lib/rails/generators/rails/app/templates/bin/yarn.tt
index c2f9b6768a..90ddcc520e 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/yarn
+++ b/railties/lib/rails/generators/rails/app/templates/bin/yarn.tt
@@ -1,7 +1,7 @@
-VENDOR_PATH = File.expand_path('..', __dir__)
-Dir.chdir(VENDOR_PATH) do
+APP_ROOT = File.expand_path('..', __dir__)
+Dir.chdir(APP_ROOT) do
begin
- exec "yarnpkg #{ARGV.join(' ')}"
+ exec "yarnpkg", *ARGV
rescue Errno::ENOENT
$stderr.puts "Yarn executable was not detected in the system."
$stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru b/railties/lib/rails/generators/rails/app/templates/config.ru.tt
index f7ba0b527b..f7ba0b527b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config.ru
+++ b/railties/lib/rails/generators/rails/app/templates/config.ru.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt
index dde09edb94..9a427113c7 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/application.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt
@@ -8,10 +8,10 @@ require "rails"
require "active_model/railtie"
require "active_job/railtie"
<%= comment_if :skip_active_record %>require "active_record/railtie"
+<%= comment_if :skip_active_storage %>require "active_storage/engine"
require "action_controller/railtie"
<%= comment_if :skip_action_mailer %>require "action_mailer/railtie"
require "action_view/railtie"
-require "active_storage/engine"
<%= comment_if :skip_action_cable %>require "action_cable/engine"
<%= comment_if :skip_sprockets %>require "sprockets/railtie"
<%= comment_if :skip_test %>require "rails/test_unit/railtie"
@@ -24,11 +24,12 @@ Bundler.require(*Rails.groups)
module <%= app_const_base %>
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
- config.load_defaults <%= Rails::VERSION::STRING.to_f %>
+ config.load_defaults <%= build(:config_target_version) %>
# Settings in config/environments/* take precedence over those specified here.
- # Application configuration should go into files in config/initializers
- # -- all .rb files in that directory are automatically loaded.
+ # Application configuration can go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded after loading
+ # the framework and any gems in your application.
<%- if options.api? -%>
# Only loads a smaller set of middleware suitable for API only apps.
diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb b/railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt
index b9e460cef3..42d46b8175 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/boot.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt
@@ -1,4 +1,6 @@
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
+<% if depend_on_bootsnap? -%>
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
+<%- end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/cable.yml b/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt
index 8e53156c71..8e53156c71 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/cable.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt
index 917b52e535..917b52e535 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt
index d40117a27f..d40117a27f 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt
index 563be77710..563be77710 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt
index 2a67bdca25..2a67bdca25 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt
index 70df04079d..70df04079d 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml.tt
index 371415e6a8..371415e6a8 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt
index 04afaa0596..04afaa0596 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt
index 6da0601b24..6da0601b24 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt
index 145cfb7f74..145cfb7f74 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml.tt
index 9510568124..9510568124 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt
index 049de65f22..049de65f22 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environment.rb b/railties/lib/rails/generators/rails/app/templates/config/environment.rb.tt
index 426333bb46..426333bb46 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environment.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/environment.rb.tt
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 98689cc30d..3807c8a9aa 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
@@ -14,7 +14,7 @@ Rails.application.configure do
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
- if Rails.root.join('tmp/caching-dev.txt').exist?
+ if Rails.root.join('tmp', 'caching-dev.txt').exist?
config.action_controller.perform_caching = true
config.cache_store = :memory_store
@@ -26,9 +26,11 @@ Rails.application.configure do
config.cache_store = :null_store
end
+ <%- unless skip_active_storage? -%>
- # Store uploaded files on the local file system (see config/storage.yml for options)
+ # Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
+ <%- end -%>
<%- unless options.skip_action_mailer? -%>
# Don't care if the mailer can't send.
@@ -44,6 +46,9 @@ Rails.application.configure do
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
+ # Highlight code that triggered database queries in logs.
+ config.active_record.verbose_query_logs = true
+
<%- end -%>
<%- unless options.skip_sprockets? -%>
# Debug mode disables concatenation and preprocessing of assets.
@@ -55,7 +60,7 @@ Rails.application.configure do
config.assets.quiet = true
<%- end -%>
- # Raises error for missing translations
+ # Raises error for missing translations.
# config.action_view.raise_on_missing_translations = true
# Use an evented file watcher to asynchronously detect changes in source code,
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 2e0b555f6f..d646694477 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
@@ -34,8 +34,6 @@ Rails.application.configure do
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
- # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
-
<%- end -%>
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = 'http://assets.example.com'
@@ -44,11 +42,13 @@ Rails.application.configure do
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
- # Store uploaded files on the local file system (see config/storage.yml for options)
+ <%- unless skip_active_storage? -%>
+ # Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
+ <%- end -%>
<%- unless options[:skip_action_cable] -%>
- # Mount Action Cable outside main process or domain
+ # Mount Action Cable outside main process or domain.
# config.action_cable.mount_path = nil
# config.action_cable.url = 'wss://example.com/cable'
# config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
@@ -67,7 +67,7 @@ Rails.application.configure do
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
- # Use a real queuing backend for Active Job (and separate queues per environment)
+ # Use a real queuing backend for Active Job (and separate queues per environment).
# config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "<%= app_name %>_#{Rails.env}"
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 a53978bc6e..82f2a8aebe 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,8 +28,11 @@ Rails.application.configure do
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
- # Store uploaded files on the local file system in a temporary directory
+ <%- unless skip_active_storage? -%>
+ # Store uploaded files on the local file system in a temporary directory.
config.active_storage.service = :test
+
+ <%- end -%>
<%- unless options.skip_action_mailer? -%>
config.action_mailer.perform_caching = false
@@ -37,11 +40,14 @@ Rails.application.configure do
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
- <%- end -%>
+ <%- end -%>
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
- # Raises error for missing translations
+ # Raises error for missing translations.
# config.action_view.raise_on_missing_translations = true
+
+ # Prevent expensive template finalization at end of test suite runs.
+ config.action_view.finalize_compiled_template_methods = false
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb.tt
index 89d2efab2b..89d2efab2b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt
index 59385cdf37..59385cdf37 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt
new file mode 100644
index 0000000000..d3bcaa5ec8
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt
@@ -0,0 +1,25 @@
+# Be sure to restart your server when you modify this file.
+
+# Define an application-wide content security policy
+# For further information see the following documentation
+# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
+
+# Rails.application.config.content_security_policy do |policy|
+# policy.default_src :self, :https
+# policy.font_src :self, :https, :data
+# policy.img_src :self, :https, :data
+# policy.object_src :none
+# policy.script_src :self, :https
+# policy.style_src :self, :https
+
+# # Specify URI for violation reports
+# # policy.report_uri "/csp-violation-report-endpoint"
+# end
+
+# If you are using UJS then enable automatic nonce generation
+# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
+
+# Report CSP violations to a specified URI
+# For further information see the following documentation:
+# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
+# Rails.application.config.content_security_policy_report_only = true
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb.tt
index 5a6a32d371..5a6a32d371 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb.tt
index 3b1c1b5ed1..3b1c1b5ed1 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt
index 4a994e1e7b..4a994e1e7b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt
index ac033bf9dc..ac033bf9dc 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt
index dc1899682b..dc1899682b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt
deleted file mode 100644
index 25dcddb27a..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt
+++ /dev/null
@@ -1,27 +0,0 @@
-# Be sure to restart your server when you modify this file.
-#
-# This file contains migration options to ease your Rails 5.2 upgrade.
-#
-# Once upgraded flip defaults one by one to migrate to the new default.
-#
-# Read the Guide for Upgrading Ruby on Rails for more info on each option.
-
-# Make Active Record use stable #cache_key alongside new #cache_version method.
-# This is needed for recyclable cache keys.
-# Rails.application.config.active_record.cache_versioning = true
-
-# Use AES 256 GCM authenticated encryption for encrypted cookies.
-# Existing cookies will be converted on read then written with the new scheme.
-# Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true
-
-# Use AES-256-GCM authenticated encryption as default cipher for encrypting messages
-# instead of AES-256-CBC, when use_authenticated_message_encryption is set to true.
-# Rails.application.config.active_support.use_authenticated_message_encryption = true
-
-# Add default protection from forgery to ActionController::Base instead of in
-# ApplicationController.
-# Rails.application.config.action_controller.default_protect_from_forgery = true
-
-# Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and
-# 'f' after migrating old data.
-# Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt
new file mode 100644
index 0000000000..179b97de4a
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt
@@ -0,0 +1,10 @@
+# Be sure to restart your server when you modify this file.
+#
+# This file contains migration options to ease your Rails 6.0 upgrade.
+#
+# Once upgraded flip defaults one by one to migrate to the new default.
+#
+# Read the Guide for Upgrading Ruby on Rails for more info on each option.
+
+# Don't force requests from old versions of IE to be UTF-8 encoded
+# Rails.application.config.action_view.default_enforce_utf8 = false
diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt
index 1e19380dcb..a5eccf816b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/puma.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt
@@ -26,31 +26,9 @@ environment ENV.fetch("RAILS_ENV") { "development" }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
-# process behavior so workers use less memory. If you use this option
-# you need to make sure to reconnect any threads in the `on_worker_boot`
-# block.
+# process behavior so workers use less memory.
#
# preload_app!
-# If you are preloading your application and using Active Record, it's
-# recommended that you close any connections to the database before workers
-# are forked to prevent connection leakage.
-#
-# before_fork do
-# ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
-# end
-
-# The code in the `on_worker_boot` will be called if you are using
-# clustered mode by specifying a number of `workers`. After each worker
-# process is booted, this block will be run. If you are using the `preload_app!`
-# option, you will want to use this block to reconnect to any threads
-# or connections that may have been created at application boot, as Ruby
-# cannot share connections between processes.
-#
-# 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.tt
index 787824f888..787824f888 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/routes.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/routes.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml
deleted file mode 100644
index ea9d47396c..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Your secret key is used for verifying the integrity of signed cookies.
-# If you change this key, all old signed cookies will become invalid!
-
-# Make sure the secret is at least 30 characters and all random,
-# no regular words or you'll be exposed to dictionary attacks.
-# You can use `rails secret` to generate a secure secret key.
-
-# Make sure the secrets in this file are kept private
-# if you're sharing your code publicly.
-
-# Shared secrets are available across all environments.
-
-# shared:
-# api_key: a1B2c3D4e5F6
-
-# Environmental secrets are only available for that specific environment.
-
-development:
- secret_key_base: <%= app_secret %>
-
-test:
- secret_key_base: <%= app_secret %>
-
-# Do not keep production secrets in the unencrypted secrets file.
-# Instead, either read values from the environment.
-# Or, use `bin/rails secrets:setup` to configure encrypted secrets
-# and move the `production:` environment over there.
-
-production:
- secret_key_base: <%%= ENV["SECRET_KEY_BASE"] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/spring.rb b/railties/lib/rails/generators/rails/app/templates/config/spring.rb.tt
index 9fa7863f99..9fa7863f99 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/spring.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/spring.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/storage.yml b/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt
index 089ed4567a..7207c75086 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/storage.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt
@@ -6,11 +6,11 @@ local:
service: Disk
root: <%%= Rails.root.join("storage") %>
-# Use rails secrets:edit to set the AWS secrets (as shared:aws:access_key_id|secret_access_key)
+# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
# service: S3
-# access_key_id: <%%= Rails.application.secrets.dig(:aws, :access_key_id) %>
-# secret_access_key: <%%= Rails.application.secrets.dig(:aws, :secret_access_key) %>
+# access_key_id: <%%= Rails.application.credentials.dig(:aws, :access_key_id) %>
+# secret_access_key: <%%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
# region: us-east-1
# bucket: your_own_bucket
@@ -18,15 +18,14 @@ local:
# google:
# service: GCS
# project: your_project
-# keyfile: <%%= Rails.root.join("path/to/gcs.keyfile") %>
+# credentials: <%%= Rails.root.join("path/to/gcs.keyfile") %>
# bucket: your_own_bucket
-# Use rails secrets:edit to set the Azure Storage secret (as shared:azure_storage:storage_access_key)
+# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
# microsoft:
# service: AzureStorage
-# path: your_azure_storage_path
# storage_account_name: your_account_name
-# storage_access_key: <%%= Rails.application.secrets.dig(:azure_storage, :storage_access_key) %>
+# storage_access_key: <%%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
# container: your_container_name
# mirror:
diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore.tt
index 83a7b211aa..4e114fb1d9 100644
--- a/railties/lib/rails/generators/rails/app/templates/gitignore
+++ b/railties/lib/rails/generators/rails/app/templates/gitignore.tt
@@ -21,14 +21,19 @@
!/tmp/.keep
<% end -%>
+<% unless skip_active_storage? -%>
# Ignore uploaded files in development
/storage/*
+<% if keeps? -%>
+!/storage/.keep
+<% end -%>
+<% end -%>
<% unless options.skip_yarn? -%>
/node_modules
/yarn-error.log
-<% end -%>
+<% end -%>
<% unless options.api? -%>
/public/assets
<% end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/package.json b/railties/lib/rails/generators/rails/app/templates/package.json.tt
index 46db57dcbe..46db57dcbe 100644
--- a/railties/lib/rails/generators/rails/app/templates/package.json
+++ b/railties/lib/rails/generators/rails/app/templates/package.json.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/ruby-version b/railties/lib/rails/generators/rails/app/templates/ruby-version
deleted file mode 100644
index c444f33b0f..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/ruby-version
+++ /dev/null
@@ -1 +0,0 @@
-<%= RUBY_VERSION -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/ruby-version.tt b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt
new file mode 100644
index 0000000000..19f0d7f202
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt
@@ -0,0 +1 @@
+<%= "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb.tt
index d19212abd5..d19212abd5 100644
--- a/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb
+++ b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt
index 6ad1f11781..c918b57eca 100644
--- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
+++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt
@@ -1,7 +1,15 @@
+ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
require 'rails/test_help'
class ActiveSupport::TestCase
+ # Run tests in parallel with specified workers
+<% if defined?(JRUBY_VERSION) -%>
+ parallelize(workers: 2, with: :threads)
+<%- else -%>
+ parallelize(workers: 2)
+<% end -%>
+
<% unless options[:skip_active_record] -%>
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
diff --git a/railties/lib/rails/generators/rails/assets/templates/stylesheet.css b/railties/lib/rails/generators/rails/assets/templates/stylesheet.css
index 7594abf268..afad32db02 100644
--- a/railties/lib/rails/generators/rails/assets/templates/stylesheet.css
+++ b/railties/lib/rails/generators/rails/assets/templates/stylesheet.css
@@ -1,4 +1,4 @@
-/*
+/*
Place all the styles related to the matching controller here.
They will automatically be included in application.css.
*/
diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb
index 6d45d6e8f8..eb75e7e661 100644
--- a/railties/lib/rails/generators/rails/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb
@@ -16,13 +16,24 @@ module Rails
def add_routes
return if options[:skip_routes]
+ return if actions.empty?
route generate_routing_code
end
- hook_for :template_engine, :test_framework, :helper, :assets
+ hook_for :template_engine, :test_framework, :helper, :assets do |generator|
+ invoke generator, [ remove_possible_suffix(name), actions ]
+ end
private
+ def file_name
+ @_file_name ||= remove_possible_suffix(super)
+ end
+
+ def remove_possible_suffix(name)
+ name.sub(/_?controller$/i, "")
+ end
+
# This method creates nested route entry for namespaced resources.
# For eg. rails g controller foo/bar/baz index show
# Will generate -
diff --git a/railties/lib/rails/generators/rails/controller/templates/controller.rb b/railties/lib/rails/generators/rails/controller/templates/controller.rb.tt
index 633e0b3177..633e0b3177 100644
--- a/railties/lib/rails/generators/rails/controller/templates/controller.rb
+++ b/railties/lib/rails/generators/rails/controller/templates/controller.rb.tt
diff --git a/railties/lib/rails/generators/rails/credentials/credentials_generator.rb b/railties/lib/rails/generators/rails/credentials/credentials_generator.rb
index ddcccd5ce5..719e0c1e4c 100644
--- a/railties/lib/rails/generators/rails/credentials/credentials_generator.rb
+++ b/railties/lib/rails/generators/rails/credentials/credentials_generator.rb
@@ -1,20 +1,17 @@
# frozen_string_literal: true
-require_relative "../../base"
-require_relative "../master_key/master_key_generator"
+require "rails/generators/base"
+require "rails/generators/rails/master_key/master_key_generator"
require "active_support/encrypted_configuration"
module Rails
module Generators
- class CredentialsGenerator < Base
- CONFIG_PATH = "config/credentials.yml.enc"
- KEY_PATH = "config/master.key"
-
+ class CredentialsGenerator < Base # :nodoc:
def add_credentials_file
- unless File.exist?(CONFIG_PATH)
+ unless credentials.content_path.exist?
template = credentials_template
- say "Adding #{CONFIG_PATH} to store encrypted credentials."
+ say "Adding #{credentials.content_path} to store encrypted credentials."
say ""
say "The following content has been encrypted with the Rails master key:"
say ""
@@ -29,17 +26,30 @@ module Rails
end
def add_credentials_file_silently(template = nil)
- unless File.exist?(CONFIG_PATH)
- setup = { config_path: CONFIG_PATH, key_path: KEY_PATH, env_key: "RAILS_MASTER_KEY" }
- ActiveSupport::EncryptedConfiguration.new(setup).write(credentials_template)
+ unless credentials.content_path.exist?
+ credentials.write(credentials_template)
end
end
private
+ def credentials
+ ActiveSupport::EncryptedConfiguration.new(
+ config_path: "config/credentials.yml.enc",
+ key_path: "config/master.key",
+ env_key: "RAILS_MASTER_KEY",
+ raise_if_missing_key: true
+ )
+ end
+
def credentials_template
- "# amazon:\n# access_key_id: 123\n# secret_access_key: 345\n\n" +
- "# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.\n" +
- "secret_key_base: #{SecureRandom.hex(64)}"
+ <<~YAML
+ # aws:
+ # access_key_id: 123
+ # secret_access_key: 345
+
+ # Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
+ secret_key_base: #{SecureRandom.hex(64)}
+ YAML
end
end
end
diff --git a/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb b/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb
new file mode 100644
index 0000000000..867e28c6db
--- /dev/null
+++ b/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "rails/generators/base"
+require "active_support/encrypted_file"
+
+module Rails
+ module Generators
+ class EncryptedFileGenerator < Base # :nodoc:
+ def add_encrypted_file_silently(file_path, key_path, template = encrypted_file_template)
+ unless File.exist?(file_path)
+ setup = { content_path: file_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true }
+ ActiveSupport::EncryptedFile.new(setup).write(template)
+ end
+ end
+
+ private
+ def encrypted_file_template
+ <<~YAML
+ # aws:
+ # access_key_id: 123
+ # secret_access_key: 345
+
+ YAML
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb b/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb
deleted file mode 100644
index d054e8cad2..0000000000
--- a/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "../../base"
-require_relative "../../../secrets"
-
-module Rails
- module Generators
- class EncryptedSecretsGenerator < Base
- def add_secrets_key_file
- unless File.exist?("config/secrets.yml.key") || File.exist?("config/secrets.yml.enc")
- key = Rails::Secrets.generate_key
-
- say "Adding config/secrets.yml.key to store the encryption key: #{key}"
- say ""
- say "Save this in a password manager your team can access."
- say ""
- say "If you lose the key, no one, including you, can access any encrypted secrets."
-
- say ""
- create_file "config/secrets.yml.key", key
- say ""
- end
- end
-
- def ignore_key_file
- if File.exist?(".gitignore")
- unless File.read(".gitignore").include?(key_ignore)
- say "Ignoring config/secrets.yml.key so it won't end up in Git history:"
- say ""
- append_to_file ".gitignore", key_ignore
- say ""
- end
- else
- say "IMPORTANT: Don't commit config/secrets.yml.key. Add this to your ignore file:"
- say key_ignore, :on_green
- say ""
- end
- end
-
- def add_encrypted_secrets_file
- unless (defined?(@@skip_secrets_file) && @@skip_secrets_file) || File.exist?("config/secrets.yml.enc")
- say "Adding config/secrets.yml.enc to store secrets that needs to be encrypted."
- say ""
- say "For now the file contains this but it's been encrypted with the generated key:"
- say ""
- say Secrets.template, :on_green
- say ""
-
- Secrets.write(Secrets.template)
-
- say "You can edit encrypted secrets with `bin/rails secrets:edit`."
- say ""
- end
-
- say "Add this to your config/environments/production.rb:"
- say "config.read_encrypted_secrets = true"
- end
-
- def self.skip_secrets_file
- @@skip_secrets_file = true
- yield
- ensure
- @@skip_secrets_file = false
- end
-
- private
- def key_ignore
- [ "", "# Ignore encrypted secrets key file.", "config/secrets.yml.key", "" ].join("\n")
- end
- end
- end
-end
diff --git a/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb
new file mode 100644
index 0000000000..e2359e9ded
--- /dev/null
+++ b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require "pathname"
+require "rails/generators/base"
+require "active_support/encrypted_file"
+
+module Rails
+ module Generators
+ class EncryptionKeyFileGenerator < Base # :nodoc:
+ def add_key_file(key_path)
+ key_path = Pathname.new(key_path)
+
+ unless key_path.exist?
+ key = ActiveSupport::EncryptedFile.generate_key
+
+ log "Adding #{key_path} to store the encryption key: #{key}"
+ log ""
+ log "Save this in a password manager your team can access."
+ log ""
+ log "If you lose the key, no one, including you, can access anything encrypted with it."
+
+ log ""
+ add_key_file_silently(key_path, key)
+ log ""
+ end
+ end
+
+ def add_key_file_silently(key_path, key = nil)
+ create_file key_path, key || ActiveSupport::EncryptedFile.generate_key
+ key_path.chmod 0600
+ end
+
+ def ignore_key_file(key_path, ignore: key_ignore(key_path))
+ if File.exist?(".gitignore")
+ unless File.read(".gitignore").include?(ignore)
+ log "Ignoring #{key_path} so it won't end up in Git history:"
+ log ""
+ append_to_file ".gitignore", ignore
+ log ""
+ end
+ else
+ log "IMPORTANT: Don't commit #{key_path}. Add this to your ignore file:"
+ log ignore, :on_green
+ log ""
+ end
+ end
+
+ def ignore_key_file_silently(key_path, ignore: key_ignore(key_path))
+ append_to_file ".gitignore", ignore if File.exist?(".gitignore")
+ end
+
+ private
+ def key_ignore(key_path)
+ [ "", "/#{key_path}", "" ].join("\n")
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/rails/helper/templates/helper.rb b/railties/lib/rails/generators/rails/helper/templates/helper.rb.tt
index b4173151b4..b4173151b4 100644
--- a/railties/lib/rails/generators/rails/helper/templates/helper.rb
+++ b/railties/lib/rails/generators/rails/helper/templates/helper.rb.tt
diff --git a/railties/lib/rails/generators/rails/master_key/master_key_generator.rb b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb
index e49d3b39e0..21664ea86d 100644
--- a/railties/lib/rails/generators/rails/master_key/master_key_generator.rb
+++ b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb
@@ -1,46 +1,48 @@
# frozen_string_literal: true
-require_relative "../../base"
require "pathname"
+require "rails/generators/base"
+require "rails/generators/rails/encryption_key_file/encryption_key_file_generator"
require "active_support/encrypted_file"
module Rails
module Generators
- class MasterKeyGenerator < Base
+ class MasterKeyGenerator < Base # :nodoc:
MASTER_KEY_PATH = Pathname.new("config/master.key")
def add_master_key_file
unless MASTER_KEY_PATH.exist?
key = ActiveSupport::EncryptedFile.generate_key
- say "Adding #{MASTER_KEY_PATH} to store the master encryption key: #{key}"
- say ""
- say "Save this in a password manager your team can access."
- say ""
- say "If you lose the key, no one, including you, can access anything encrypted with it."
+ log "Adding #{MASTER_KEY_PATH} to store the master encryption key: #{key}"
+ log ""
+ log "Save this in a password manager your team can access."
+ log ""
+ log "If you lose the key, no one, including you, can access anything encrypted with it."
- say ""
- create_file MASTER_KEY_PATH, key
- say ""
+ log ""
+ add_master_key_file_silently(key)
+ log ""
+ end
+ end
- ignore_master_key_file
+ def add_master_key_file_silently(key = nil)
+ unless MASTER_KEY_PATH.exist?
+ key_file_generator.add_key_file_silently(MASTER_KEY_PATH, key)
end
end
+ def ignore_master_key_file
+ key_file_generator.ignore_key_file(MASTER_KEY_PATH, ignore: key_ignore)
+ end
+
+ def ignore_master_key_file_silently
+ key_file_generator.ignore_key_file_silently(MASTER_KEY_PATH, ignore: key_ignore)
+ end
+
private
- def ignore_master_key_file
- if File.exist?(".gitignore")
- unless File.read(".gitignore").include?(key_ignore)
- say "Ignoring #{MASTER_KEY_PATH} so it won't end up in Git history:"
- say ""
- append_to_file ".gitignore", key_ignore
- say ""
- end
- else
- say "IMPORTANT: Don't commit #{MASTER_KEY_PATH}. Add this to your ignore file:"
- say key_ignore, :on_green
- say ""
- end
+ def key_file_generator
+ EncryptionKeyFileGenerator.new([], options)
end
def key_ignore
diff --git a/railties/lib/rails/generators/rails/model/model_generator.rb b/railties/lib/rails/generators/rails/model/model_generator.rb
index 1dca03e0bb..de4de2cae2 100644
--- a/railties/lib/rails/generators/rails/model/model_generator.rb
+++ b/railties/lib/rails/generators/rails/model/model_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../model_helpers"
+require "rails/generators/model_helpers"
module Rails
module Generators
diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
index eb941adf95..a83c911806 100644
--- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
+++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
-require "active_support/core_ext/hash/slice"
-require_relative "../app/app_generator"
+require "rails/generators/rails/app/app_generator"
require "date"
module Rails
@@ -88,17 +87,18 @@ task default: :test
end
PASSTHROUGH_OPTIONS = [
- :skip_active_record, :skip_action_mailer, :skip_javascript, :skip_action_cable, :skip_sprockets, :database,
+ :skip_active_record, :skip_active_storage, :skip_action_mailer, :skip_javascript, :skip_action_cable, :skip_sprockets, :database,
:javascript, :skip_yarn, :api, :quiet, :pretend, :skip
]
def generate_test_dummy(force = false)
- opts = (options || {}).slice(*PASSTHROUGH_OPTIONS)
+ opts = (options.dup || {}).keep_if { |k, _| PASSTHROUGH_OPTIONS.map(&:to_s).include?(k) }
opts[:force] = force
opts[:skip_bundle] = true
opts[:skip_listen] = true
opts[:skip_git] = true
opts[:skip_turbolinks] = true
+ opts[:dummy_app] = true
invoke Rails::Generators::AppGenerator,
[ File.expand_path(dummy_path, destination_root) ], opts
@@ -167,7 +167,7 @@ task default: :test
gemfile_in_app_path = File.join(rails_app_path, "Gemfile")
if File.exist? gemfile_in_app_path
- entry = "gem '#{name}', path: '#{relative_path}'"
+ entry = "\ngem '#{name}', path: '#{relative_path}'"
append_file gemfile_in_app_path, entry
end
end
@@ -263,6 +263,10 @@ task default: :test
public_task :apply_rails_template
def run_after_bundle_callbacks
+ unless @after_bundle_callbacks.empty?
+ ActiveSupport::Deprecation.warn("`after_bundle` is deprecated and will be removed in the next version of Rails. ")
+ end
+
@after_bundle_callbacks.each do |callback|
callback.call
end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt
index 9a8c4bf098..9a8c4bf098 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec
+++ b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile.tt
index 290259b4db..290259b4db 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE.tt
index ff2fb3ba4e..ff2fb3ba4e 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE
+++ b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/README.md b/railties/lib/rails/generators/rails/plugin/templates/README.md.tt
index 1632409bea..1632409bea 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/README.md
+++ b/railties/lib/rails/generators/rails/plugin/templates/README.md.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt
index f3efe21cf1..f3efe21cf1 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile
+++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
index abbacd9bec..b86ef0f2f8 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class ApplicationController < ActionController::#{api? ? "API" : "Base"}
#{ api? ? '# ' : '' }protect_from_forgery with: :exception
end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt
index 25d692732d..be078f36de 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
module ApplicationHelper
end
rb
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt
index bad1ff2d16..846863bc13 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class ApplicationJob < ActiveJob::Base
end
rb
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt
index 09aac13f42..246e274348 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt
index 8aa3de78f1..21465278be 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt
index 6bc480161d..6e54a1ce9d 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt
@@ -2,9 +2,13 @@
<html>
<head>
<title><%= humanized %></title>
+ <%%= csrf_meta_tags %>
+ <%%= csp_meta_tag %>
+
<%%= stylesheet_link_tag "<%= namespaced_name %>/application", media: "all" %>
+ <%- unless options[:skip_javascript] -%>
<%%= javascript_include_tag "<%= namespaced_name %>/application" %>
- <%%= csrf_meta_tags %>
+ <%- end -%>
</head>
<body>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
index b3264509fc..ee8e469da2 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
@@ -19,10 +19,10 @@ require "rails"
require "active_model/railtie"
require "active_job/railtie"
<%= comment_if :skip_active_record %>require "active_record/railtie"
+<%= comment_if :skip_active_storage %>require "active_storage/engine"
require "action_controller/railtie"
<%= comment_if :skip_action_mailer %>require "action_mailer/railtie"
require "action_view/railtie"
-require "active_storage/engine"
<%= comment_if :skip_action_cable %>require "action_cable/engine"
<%= comment_if :skip_sprockets %>require "sprockets/railtie"
<%= comment_if :skip_test %>require "rails/test_unit/railtie"
diff --git a/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb.tt
index 154452bfe5..154452bfe5 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/gitignore b/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt
index e15863d860..7a68da5c4b 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/gitignore
+++ b/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt
@@ -11,5 +11,8 @@ pkg/
<%= dummy_path %>/node_modules/
<%= dummy_path %>/yarn-error.log
<% end -%>
+<% unless skip_active_storage? -%>
+<%= dummy_path %>/storage/
+<% end -%>
<%= dummy_path %>/tmp/
<% end -%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb.tt
index 3285055eb7..3285055eb7 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt
index 8938770fc4..4ec1804940 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class Engine < ::Rails::Engine
#{mountable? ? ' isolate_namespace ' + camelized_modules : ' '}
#{api? ? " config.generators.api_only = true" : ' '}
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt
index 7bdf4ee5fb..b853fabcc3 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class Railtie < ::Rails::Railtie
end
rb
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt
index b08f4ef9ae..b08f4ef9ae 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt
index 88a2c4120f..88a2c4120f 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb.tt
index 47b56ae3df..06ffe2f1ed 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb.tt
@@ -8,10 +8,10 @@ require "rails"
require "active_model/railtie"
require "active_job/railtie"
<%= comment_if :skip_active_record %>require "active_record/railtie"
+<%= comment_if :skip_active_storage %>require "active_storage/engine"
require "action_controller/railtie"
<%= comment_if :skip_action_mailer %>require "action_mailer/railtie"
require "action_view/railtie"
-require "active_storage/engine"
<%= comment_if :skip_action_cable %>require "action_cable/engine"
<%= comment_if :skip_sprockets %>require "sprockets/railtie"
<%= comment_if :skip_test %>require "rails/test_unit/railtie"
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb.tt
index c9aef85d40..c9aef85d40 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js.tt
index 03937cf8ff..03937cf8ff 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt
index 2f23844f5e..2f23844f5e 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt
index e54c6461cc..51049826bf 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt
@@ -10,4 +10,8 @@
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
+//= require rails-ujs
+<% unless skip_active_storage? -%>
+//= require activestorage
+<% end -%>
//= require_tree .
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb.tt
index 694510edc0..694510edc0 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt
index 1ee05d7871..1ee05d7871 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.tt
index d19212abd5..d19212abd5 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb.tt
index 29e59d8407..29e59d8407 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt
index 7fa9973931..755d19ef5d 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt
@@ -1,3 +1,6 @@
+# Configure Rails Environment
+ENV["RAILS_ENV"] = "test"
+
require_relative "<%= File.join('..', options[:dummy_path], 'config/environment') -%>"
<% unless options[:skip_active_record] -%>
ActiveRecord::Migrator.migrations_paths = [File.expand_path("../<%= options[:dummy_path] -%>/db/migrate", __dir__)]
diff --git a/railties/lib/rails/generators/rails/resource/USAGE b/railties/lib/rails/generators/rails/resource/USAGE
index e359cd574f..66d0ee546a 100644
--- a/railties/lib/rails/generators/rails/resource/USAGE
+++ b/railties/lib/rails/generators/rails/resource/USAGE
@@ -1,6 +1,6 @@
Description:
Stubs out a new resource including an empty model and controller suitable
- for a restful, resource-oriented application. Pass the singular model name,
+ for a RESTful, resource-oriented application. Pass the singular model name,
either CamelCased or under_scored, as the first argument, and an optional
list of attribute pairs.
diff --git a/railties/lib/rails/generators/rails/resource/resource_generator.rb b/railties/lib/rails/generators/rails/resource/resource_generator.rb
index 74b1574863..3ba25ef0fe 100644
--- a/railties/lib/rails/generators/rails/resource/resource_generator.rb
+++ b/railties/lib/rails/generators/rails/resource/resource_generator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../../resource_helpers"
-require_relative "../model/model_generator"
+require "rails/generators/resource_helpers"
+require "rails/generators/rails/model/model_generator"
module Rails
module Generators
diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
index c0fb873d36..8beb7416c0 100644
--- a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../resource/resource_generator"
+require "rails/generators/rails/resource/resource_generator"
module Rails
module Generators
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
index 601a4b3a6e..7030561a33 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
+++ b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../resource_helpers"
+require "rails/generators/resource_helpers"
module Rails
module Generators
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt
index 400afec6dc..400afec6dc 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb
+++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt
index 42b9e34274..05f1c2b2d3 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
+++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt
@@ -29,7 +29,7 @@ class <%= controller_class_name %>Controller < ApplicationController
@<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
if @<%= orm_instance.save %>
- redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %>
+ redirect_to <%= redirect_resource_name %>, notice: <%= "'#{human_name} was successfully created.'" %>
else
render :new
end
@@ -38,7 +38,7 @@ class <%= controller_class_name %>Controller < ApplicationController
# PATCH/PUT <%= route_url %>/1
def update
if @<%= orm_instance.update("#{singular_table_name}_params") %>
- redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %>
+ redirect_to <%= redirect_resource_name %>, notice: <%= "'#{human_name} was successfully updated.'" %>
else
render :edit
end
diff --git a/railties/lib/rails/generators/rails/task/templates/task.rb b/railties/lib/rails/generators/rails/task/templates/task.rb.tt
index 1e3ed5f158..1e3ed5f158 100644
--- a/railties/lib/rails/generators/rails/task/templates/task.rb
+++ b/railties/lib/rails/generators/rails/task/templates/task.rb.tt
diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb
index 8bd5110940..5675faff70 100644
--- a/railties/lib/rails/generators/resource_helpers.rb
+++ b/railties/lib/rails/generators/resource_helpers.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "active_model"
-require_relative "model_helpers"
+require "rails/generators/active_model"
+require "rails/generators/model_helpers"
module Rails
module Generators
@@ -25,13 +25,8 @@ module Rails
assign_controller_names!(controller_name.pluralize)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :controller_name, :controller_file_name
-
private
+ attr_reader :controller_name, :controller_file_name
def controller_class_path
if options[:model_name]
diff --git a/railties/lib/rails/generators/test_case.rb b/railties/lib/rails/generators/test_case.rb
index 9c06eed95a..5c71bf0be9 100644
--- a/railties/lib/rails/generators/test_case.rb
+++ b/railties/lib/rails/generators/test_case.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-require_relative "../generators"
-require_relative "testing/behaviour"
-require_relative "testing/setup_and_teardown"
-require_relative "testing/assertions"
+require "rails/generators"
+require "rails/generators/testing/behaviour"
+require "rails/generators/testing/setup_and_teardown"
+require "rails/generators/testing/assertions"
require "fileutils"
module Rails
diff --git a/railties/lib/rails/generators/test_unit.rb b/railties/lib/rails/generators/test_unit.rb
index 1aa03ace99..1005ac557c 100644
--- a/railties/lib/rails/generators/test_unit.rb
+++ b/railties/lib/rails/generators/test_unit.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "named_base"
+require "rails/generators/named_base"
module TestUnit # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/test_unit/controller/controller_generator.rb b/railties/lib/rails/generators/test_unit/controller/controller_generator.rb
index 3947b74205..1a9ac6bf2a 100644
--- a/railties/lib/rails/generators/test_unit/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/test_unit/controller/controller_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../test_unit"
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt
index ff41fef9e9..ff41fef9e9 100644
--- a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb
+++ b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/generator/generator_generator.rb b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb
index 9f79e9eee1..19be4f2f51 100644
--- a/railties/lib/rails/generators/test_unit/generator/generator_generator.rb
+++ b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../test_unit"
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb.tt
index a7f1fc4fba..a7f1fc4fba 100644
--- a/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb
+++ b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/helper/helper_generator.rb b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb
index 1c60c3573f..77308dcf7d 100644
--- a/railties/lib/rails/generators/test_unit/helper/helper_generator.rb
+++ b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../test_unit"
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/test_unit/integration/integration_generator.rb b/railties/lib/rails/generators/test_unit/integration/integration_generator.rb
index c859ba5e4e..ae307c5cd9 100644
--- a/railties/lib/rails/generators/test_unit/integration/integration_generator.rb
+++ b/railties/lib/rails/generators/test_unit/integration/integration_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../test_unit"
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb.tt
index 118e0f1271..118e0f1271 100644
--- a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb
+++ b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/job/job_generator.rb b/railties/lib/rails/generators/test_unit/job/job_generator.rb
index cbbcd1cd4e..1dae3cb6a5 100644
--- a/railties/lib/rails/generators/test_unit/job/job_generator.rb
+++ b/railties/lib/rails/generators/test_unit/job/job_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../test_unit"
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
@@ -8,8 +8,13 @@ module TestUnit # :nodoc:
check_class_collision suffix: "JobTest"
def create_test_file
- template "unit_test.rb.erb", File.join("test/jobs", class_path, "#{file_name}_job_test.rb")
+ template "unit_test.rb", File.join("test/jobs", class_path, "#{file_name}_job_test.rb")
end
+
+ private
+ def file_name
+ @_file_name ||= super.sub(/_job\z/i, "")
+ end
end
end
end
diff --git a/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb b/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.tt
index f5351d0ec6..f5351d0ec6 100644
--- a/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb
+++ b/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.tt
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 45f82158e9..ab8331f31c 100644
--- a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
+++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../test_unit"
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
@@ -21,7 +21,7 @@ module TestUnit # :nodoc:
private
def file_name
- @_file_name ||= super.gsub(/_mailer/i, "")
+ @_file_name ||= super.sub(/_mailer\z/i, "")
end
end
end
diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb.tt
index a2f2d30de5..a2f2d30de5 100644
--- a/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb
+++ b/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb.tt
index b063cbc47b..b063cbc47b 100644
--- a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb
+++ b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/model/model_generator.rb b/railties/lib/rails/generators/test_unit/model/model_generator.rb
index 2b1cc50c20..02d7502592 100644
--- a/railties/lib/rails/generators/test_unit/model/model_generator.rb
+++ b/railties/lib/rails/generators/test_unit/model/model_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../test_unit"
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt
index 0681780c97..0681780c97 100644
--- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml
+++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt
diff --git a/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb b/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb.tt
index c9bc7d5b90..c9bc7d5b90 100644
--- a/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb
+++ b/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb b/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb
index 10fe27b2c3..0657bc2389 100644
--- a/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb
+++ b/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../test_unit"
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb
index 23d5c63614..e2e8b18eab 100644
--- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../../test_unit"
-require_relative "../../resource_helpers"
+require "rails/generators/test_unit"
+require "rails/generators/resource_helpers"
module TestUnit # :nodoc:
module Generators # :nodoc:
@@ -23,7 +23,7 @@ module TestUnit # :nodoc:
template template_file,
File.join("test/controllers", controller_class_path, "#{controller_file_name}_controller_test.rb")
- unless options.api? || options[:system_tests].nil?
+ if !options.api? && options[:system_tests]
template "system_test.rb", File.join("test/system", class_path, "#{file_name.pluralize}_test.rb")
end
end
diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb.tt
index f21861d8e6..f21861d8e6 100644
--- a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb
+++ b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb.tt
index 195d60be20..195d60be20 100644
--- a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb
+++ b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt
index f83f5a5c62..f83f5a5c62 100644
--- a/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb
+++ b/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/system/system_generator.rb b/railties/lib/rails/generators/test_unit/system/system_generator.rb
index d3acbfd738..08504d4124 100644
--- a/railties/lib/rails/generators/test_unit/system/system_generator.rb
+++ b/railties/lib/rails/generators/test_unit/system/system_generator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../test_unit"
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb.tt
index d19212abd5..d19212abd5 100644
--- a/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb
+++ b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/system/templates/system_test.rb b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb.tt
index b5ce2ba5c8..b5ce2ba5c8 100644
--- a/railties/lib/rails/generators/test_unit/system/templates/system_test.rb
+++ b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb.tt
diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb
index 97a09c1166..6ab88bd59f 100644
--- a/railties/lib/rails/generators/testing/behaviour.rb
+++ b/railties/lib/rails/generators/testing/behaviour.rb
@@ -6,7 +6,7 @@ require "active_support/core_ext/hash/reverse_merge"
require "active_support/core_ext/kernel/reporting"
require "active_support/testing/stream"
require "active_support/concern"
-require_relative "../../generators"
+require "rails/generators"
module Rails
module Generators
diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb
index d8f361f524..d5c9973c6b 100644
--- a/railties/lib/rails/info.rb
+++ b/railties/lib/rails/info.rb
@@ -99,7 +99,7 @@ module Rails
end
property "Database schema version" do
- ActiveRecord::Migrator.current_version rescue nil
+ ActiveRecord::Base.connection.migration_context.current_version rescue nil
end
end
end
diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb
index 6535b5b6ad..b4f4a5922a 100644
--- a/railties/lib/rails/info_controller.rb
+++ b/railties/lib/rails/info_controller.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "application_controller"
+require "rails/application_controller"
require "action_dispatch/routing/inspector"
class Rails::InfoController < Rails::ApplicationController # :nodoc:
diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb
index 6bd3161fdd..0b0e802358 100644
--- a/railties/lib/rails/mailers_controller.rb
+++ b/railties/lib/rails/mailers_controller.rb
@@ -1,14 +1,14 @@
# frozen_string_literal: true
-require_relative "application_controller"
+require "rails/application_controller"
class Rails::MailersController < Rails::ApplicationController # :nodoc:
prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH
before_action :require_local!, unless: :show_previews?
- before_action :find_preview, only: :preview
+ before_action :find_preview, :set_locale, only: :preview
- helper_method :part_query
+ helper_method :part_query, :locale_query
def index
@previews = ActionMailer::Preview.all
@@ -84,4 +84,12 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc:
def part_query(mime_type)
request.query_parameters.merge(part: mime_type).to_query
end
+
+ def locale_query(locale)
+ request.query_parameters.merge(locale: locale).to_query
+ end
+
+ def set_locale
+ I18n.locale = params[:locale] || I18n.default_locale
+ end
end
diff --git a/railties/lib/rails/plugin/test.rb b/railties/lib/rails/plugin/test.rb
index 2426521e35..18b6fd1757 100644
--- a/railties/lib/rails/plugin/test.rb
+++ b/railties/lib/rails/plugin/test.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "../test_unit/runner"
-require_relative "../test_unit/reporter"
+require "rails/test_unit/runner"
+require "rails/test_unit/reporter"
Rails::TestUnitReporter.executable = "bin/test"
diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb
index ec5212ee76..4ea7e40319 100644
--- a/railties/lib/rails/rack/logger.rb
+++ b/railties/lib/rails/rack/logger.rb
@@ -35,9 +35,9 @@ module Rails
instrumenter = ActiveSupport::Notifications.instrumenter
instrumenter.start "request.action_dispatch", request: request
logger.info { started_request_message(request) }
- resp = @app.call(env)
- resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) }
- resp
+ status, headers, body = @app.call(env)
+ body = ::Rack::BodyProxy.new(body) { finish(request) }
+ [status, headers, body]
rescue Exception
finish(request)
raise
diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb
index 42c4a82b1b..88dd932370 100644
--- a/railties/lib/rails/railtie.rb
+++ b/railties/lib/rails/railtie.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "initializable"
+require "rails/initializable"
require "active_support/inflector"
require "active_support/core_ext/module/introspection"
require "active_support/core_ext/module/delegation"
diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb
index d9997759f2..70274b948c 100644
--- a/railties/lib/rails/railtie/configuration.rb
+++ b/railties/lib/rails/railtie/configuration.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../configuration"
+require "rails/configuration"
module Rails
class Railtie
@@ -55,7 +55,7 @@ module Rails
ActiveSupport.on_load(:before_configuration, yield: true, &block)
end
- # Third configurable block to run. Does not run if +config.cache_classes+
+ # Third configurable block to run. Does not run if +config.eager_load+
# set to false.
def before_eager_load(&block)
ActiveSupport.on_load(:before_eager_load, yield: true, &block)
diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb
index 76b6b80d28..b2d44d9b8e 100644
--- a/railties/lib/rails/ruby_version_check.rb
+++ b/railties/lib/rails/ruby_version_check.rb
@@ -1,15 +1,15 @@
# frozen_string_literal: true
-if RUBY_VERSION < "2.2.2" && RUBY_ENGINE == "ruby"
+if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.4.1") && RUBY_ENGINE == "ruby"
desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})"
abort <<-end_message
- Rails 5 requires Ruby 2.2.2 or newer.
+ Rails 6 requires Ruby 2.4.1 or newer.
You're running
#{desc}
- Please upgrade to Ruby 2.2.2 or newer to continue.
+ Please upgrade to Ruby 2.4.1 or newer to continue.
end_message
end
diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb
index aea72b2d01..747cf31d7a 100644
--- a/railties/lib/rails/secrets.rb
+++ b/railties/lib/rails/secrets.rb
@@ -2,7 +2,6 @@
require "yaml"
require "active_support/message_encryptor"
-require "active_support/core_ext/string/strip"
module Rails
# Greatly inspired by Ara T. Howard's magnificent sekrets gem. 😘
@@ -32,23 +31,10 @@ module Rails
end
end
- def generate_key
- SecureRandom.hex(OpenSSL::Cipher.new(@cipher).key_len)
- end
-
def key
ENV["RAILS_MASTER_KEY"] || read_key_file || handle_missing_key
end
- def template
- <<-end_of_template.strip_heredoc
- # See `secrets.yml` for tips on generating suitable keys.
- # production:
- # external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289
-
- end_of_template
- end
-
def encrypt(data)
encryptor.encrypt_and_sign(data)
end
@@ -70,10 +56,6 @@ module Rails
writing(read, &block)
end
- def read_template_for_editing(&block)
- writing(template, &block)
- end
-
private
def handle_missing_key
raise MissingKeyError
diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb
index 1db6c98537..7257aaeaae 100644
--- a/railties/lib/rails/source_annotation_extractor.rb
+++ b/railties/lib/rails/source_annotation_extractor.rb
@@ -1,141 +1,150 @@
# frozen_string_literal: true
-# Implements the logic behind the rake tasks for annotations like
-#
-# rails notes
-# rails notes:optimize
-#
-# and friends. See <tt>rails -T notes</tt> and <tt>railties/lib/rails/tasks/annotations.rake</tt>.
-#
-# Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that
-# represent the line where the annotation lives, its tag, and its text. Note
-# the filename is not stored.
-#
-# Annotations are looked for in comments and modulus whitespace they have to
-# start with the tag optionally followed by a colon. Everything up to the end
-# of the line (or closing ERB comment tag) is considered to be their text.
-class SourceAnnotationExtractor
- Annotation = Struct.new(:line, :tag, :text) do
- def self.directories
- @@directories ||= %w(app config db lib test) + (ENV["SOURCE_ANNOTATION_DIRECTORIES"] || "").split(",")
- end
+require "active_support/deprecation"
- # Registers additional directories to be included
- # SourceAnnotationExtractor::Annotation.register_directories("spec", "another")
- def self.register_directories(*dirs)
- directories.push(*dirs)
- end
+# Remove this deprecated class in the next minor version
+#:nodoc:
+SourceAnnotationExtractor = ActiveSupport::Deprecation::DeprecatedConstantProxy.
+ new("SourceAnnotationExtractor", "Rails::SourceAnnotationExtractor")
- def self.extensions
- @@extensions ||= {}
- end
+module Rails
+ # Implements the logic behind the rake tasks for annotations like
+ #
+ # rails notes
+ # rails notes:optimize
+ #
+ # and friends. See <tt>rails -T notes</tt> and <tt>railties/lib/rails/tasks/annotations.rake</tt>.
+ #
+ # Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that
+ # represent the line where the annotation lives, its tag, and its text. Note
+ # the filename is not stored.
+ #
+ # Annotations are looked for in comments and modulus whitespace they have to
+ # start with the tag optionally followed by a colon. Everything up to the end
+ # of the line (or closing ERB comment tag) is considered to be their text.
+ class SourceAnnotationExtractor
+ class Annotation < Struct.new(:line, :tag, :text)
+ def self.directories
+ @@directories ||= %w(app config db lib test) + (ENV["SOURCE_ANNOTATION_DIRECTORIES"] || "").split(",")
+ end
- # Registers new Annotations File Extensions
- # SourceAnnotationExtractor::Annotation.register_extensions("css", "scss", "sass", "less", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
- def self.register_extensions(*exts, &block)
- extensions[/\.(#{exts.join("|")})$/] = block
- end
+ # Registers additional directories to be included
+ # Rails::SourceAnnotationExtractor::Annotation.register_directories("spec", "another")
+ def self.register_directories(*dirs)
+ directories.push(*dirs)
+ end
- register_extensions("builder", "rb", "rake", "yml", "yaml", "ruby") { |tag| /#\s*(#{tag}):?\s*(.*)$/ }
- register_extensions("css", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
- register_extensions("erb") { |tag| /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ }
+ def self.extensions
+ @@extensions ||= {}
+ end
- # Returns a representation of the annotation that looks like this:
+ # Registers new Annotations File Extensions
+ # Rails::SourceAnnotationExtractor::Annotation.register_extensions("css", "scss", "sass", "less", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
+ def self.register_extensions(*exts, &block)
+ extensions[/\.(#{exts.join("|")})$/] = block
+ end
+
+ register_extensions("builder", "rb", "rake", "yml", "yaml", "ruby") { |tag| /#\s*(#{tag}):?\s*(.*)$/ }
+ register_extensions("css", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
+ register_extensions("erb") { |tag| /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ }
+
+ # Returns a representation of the annotation that looks like this:
+ #
+ # [126] [TODO] This algorithm is simple and clearly correct, make it faster.
+ #
+ # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above.
+ # Otherwise the string contains just line and text.
+ def to_s(options = {})
+ s = "[#{line.to_s.rjust(options[:indent])}] ".dup
+ s << "[#{tag}] " if options[:tag]
+ s << text
+ end
+ end
+
+ # Prints all annotations with tag +tag+ under the root directories +app+,
+ # +config+, +db+, +lib+, and +test+ (recursively).
#
- # [126] [TODO] This algorithm is simple and clearly correct, make it faster.
+ # Additional directories may be added using a comma-delimited list set using
+ # <tt>ENV['SOURCE_ANNOTATION_DIRECTORIES']</tt>.
#
- # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above.
- # Otherwise the string contains just line and text.
- def to_s(options = {})
- s = "[#{line.to_s.rjust(options[:indent])}] ".dup
- s << "[#{tag}] " if options[:tag]
- s << text
+ # Directories may also be explicitly set using the <tt>:dirs</tt> key in +options+.
+ #
+ # Rails::SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true
+ #
+ # If +options+ has a <tt>:tag</tt> flag, it will be passed to each annotation's +to_s+.
+ #
+ # See <tt>#find_in</tt> for a list of file extensions that will be taken into account.
+ #
+ # This class method is the single entry point for the rake tasks.
+ def self.enumerate(tag, options = {})
+ extractor = new(tag)
+ dirs = options.delete(:dirs) || Annotation.directories
+ extractor.display(extractor.find(dirs), options)
end
- end
- # Prints all annotations with tag +tag+ under the root directories +app+,
- # +config+, +db+, +lib+, and +test+ (recursively).
- #
- # Additional directories may be added using a comma-delimited list set using
- # <tt>ENV['SOURCE_ANNOTATION_DIRECTORIES']</tt>.
- #
- # Directories may also be explicitly set using the <tt>:dirs</tt> key in +options+.
- #
- # SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true
- #
- # If +options+ has a <tt>:tag</tt> flag, it will be passed to each annotation's +to_s+.
- #
- # See <tt>#find_in</tt> for a list of file extensions that will be taken into account.
- #
- # This class method is the single entry point for the rake tasks.
- def self.enumerate(tag, options = {})
- extractor = new(tag)
- dirs = options.delete(:dirs) || Annotation.directories
- extractor.display(extractor.find(dirs), options)
- end
-
- attr_reader :tag
-
- def initialize(tag)
- @tag = tag
- end
+ attr_reader :tag
- # Returns a hash that maps filenames under +dirs+ (recursively) to arrays
- # with their annotations.
- def find(dirs)
- dirs.inject({}) { |h, dir| h.update(find_in(dir)) }
- end
+ def initialize(tag)
+ @tag = tag
+ end
- # Returns a hash that maps filenames under +dir+ (recursively) to arrays
- # with their annotations. Only files with annotations are included. Files
- # with extension +.builder+, +.rb+, +.rake+, +.yml+, +.yaml+, +.ruby+,
- # +.css+, +.js+ and +.erb+ are taken into account.
- def find_in(dir)
- results = {}
-
- Dir.glob("#{dir}/*") do |item|
- next if File.basename(item)[0] == ?.
-
- if File.directory?(item)
- results.update(find_in(item))
- else
- extension = Annotation.extensions.detect do |regexp, _block|
- regexp.match(item)
- end
+ # Returns a hash that maps filenames under +dirs+ (recursively) to arrays
+ # with their annotations.
+ def find(dirs)
+ dirs.inject({}) { |h, dir| h.update(find_in(dir)) }
+ end
- if extension
- pattern = extension.last.call(tag)
- results.update(extract_annotations_from(item, pattern)) if pattern
+ # Returns a hash that maps filenames under +dir+ (recursively) to arrays
+ # with their annotations. Files with extensions registered in
+ # <tt>Rails::SourceAnnotationExtractor::Annotation.extensions</tt> are
+ # taken into account. Only files with annotations are included.
+ def find_in(dir)
+ results = {}
+
+ Dir.glob("#{dir}/*") do |item|
+ next if File.basename(item)[0] == ?.
+
+ if File.directory?(item)
+ results.update(find_in(item))
+ else
+ extension = Annotation.extensions.detect do |regexp, _block|
+ regexp.match(item)
+ end
+
+ if extension
+ pattern = extension.last.call(tag)
+ results.update(extract_annotations_from(item, pattern)) if pattern
+ end
end
end
- end
- results
- end
+ results
+ end
- # If +file+ is the filename of a file that contains annotations this method returns
- # a hash with a single entry that maps +file+ to an array of its annotations.
- # Otherwise it returns an empty hash.
- def extract_annotations_from(file, pattern)
- lineno = 0
- result = File.readlines(file, encoding: Encoding::BINARY).inject([]) do |list, line|
- lineno += 1
- next list unless line =~ pattern
- list << Annotation.new(lineno, $1, $2)
+ # If +file+ is the filename of a file that contains annotations this method returns
+ # a hash with a single entry that maps +file+ to an array of its annotations.
+ # Otherwise it returns an empty hash.
+ def extract_annotations_from(file, pattern)
+ lineno = 0
+ result = File.readlines(file, encoding: Encoding::BINARY).inject([]) do |list, line|
+ lineno += 1
+ next list unless line =~ pattern
+ list << Annotation.new(lineno, $1, $2)
+ end
+ result.empty? ? {} : { file => result }
end
- result.empty? ? {} : { file => result }
- end
- # Prints the mapping from filenames to annotations in +results+ ordered by filename.
- # The +options+ hash is passed to each annotation's +to_s+.
- def display(results, options = {})
- options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size
- results.keys.sort.each do |file|
- puts "#{file}:"
- results[file].each do |note|
- puts " * #{note.to_s(options)}"
+ # Prints the mapping from filenames to annotations in +results+ ordered by filename.
+ # The +options+ hash is passed to each annotation's +to_s+.
+ def display(results, options = {})
+ options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size
+ results.keys.sort.each do |file|
+ puts "#{file}:"
+ results[file].each do |note|
+ puts " * #{note.to_s(options)}"
+ end
+ puts
end
- puts
end
end
end
diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb
index 2f644a20c9..56f2eba312 100644
--- a/railties/lib/rails/tasks.rb
+++ b/railties/lib/rails/tasks.rb
@@ -12,7 +12,6 @@ require "rake"
middleware
misc
restart
- routes
tmp
yarn
).tap { |arr|
diff --git a/railties/lib/rails/tasks/annotations.rake b/railties/lib/rails/tasks/annotations.rake
index 931f81ac7e..60bcdc5e1b 100644
--- a/railties/lib/rails/tasks/annotations.rake
+++ b/railties/lib/rails/tasks/annotations.rake
@@ -1,22 +1,22 @@
# frozen_string_literal: true
-require_relative "../source_annotation_extractor"
+require "rails/source_annotation_extractor"
desc "Enumerate all annotations (use notes:optimize, :fixme, :todo for focus)"
task :notes do
- SourceAnnotationExtractor.enumerate "OPTIMIZE|FIXME|TODO", tag: true
+ Rails::SourceAnnotationExtractor.enumerate "OPTIMIZE|FIXME|TODO", tag: true
end
namespace :notes do
["OPTIMIZE", "FIXME", "TODO"].each do |annotation|
# desc "Enumerate all #{annotation} annotations"
task annotation.downcase.intern do
- SourceAnnotationExtractor.enumerate annotation
+ Rails::SourceAnnotationExtractor.enumerate annotation
end
end
desc "Enumerate a custom annotation, specify with ANNOTATION=CUSTOM"
task :custom do
- SourceAnnotationExtractor.enumerate ENV["ANNOTATION"]
+ Rails::SourceAnnotationExtractor.enumerate ENV["ANNOTATION"]
end
end
diff --git a/railties/lib/rails/tasks/dev.rake b/railties/lib/rails/tasks/dev.rake
index 90fd3ccd8b..5aea6f7dc5 100644
--- a/railties/lib/rails/tasks/dev.rake
+++ b/railties/lib/rails/tasks/dev.rake
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../dev_caching"
+require "rails/dev_caching"
namespace :dev do
desc "Toggle development mode caching on/off"
diff --git a/railties/lib/rails/tasks/engine.rake b/railties/lib/rails/tasks/engine.rake
index 3ce49a1641..8d77904210 100644
--- a/railties/lib/rails/tasks/engine.rake
+++ b/railties/lib/rails/tasks/engine.rake
@@ -8,7 +8,7 @@ task "load_app" do
task update: [ "update:bin" ]
namespace :update do
- require_relative "../engine/updater"
+ require "rails/engine/updater"
# desc "Adds new executables to the engine bin/ directory"
task :bin do
Rails::Engine::Updater.run(:create_bin_files)
@@ -53,7 +53,7 @@ namespace :db do
desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)."
app_task "rollback"
- desc "Create a db/schema.rb file that can be portably used against any DB supported by Active Record"
+ desc "Create a db/schema.rb file that can be portably used against any database supported by Active Record"
app_task "schema:dump"
desc "Load a schema.rb file into the database"
@@ -62,7 +62,7 @@ namespace :db do
desc "Load the seed data from db/seeds.rb"
app_task "seed"
- desc "Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)"
+ desc "Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)"
app_task "setup"
desc "Dump the database structure to an SQL file"
@@ -70,6 +70,9 @@ namespace :db do
desc "Retrieves the current schema version number"
app_task "version"
+
+ # desc 'Load the test schema'
+ app_task "test:prepare"
end
def find_engine_path(path)
diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake
index 8931aabcda..7dfcd14bd0 100644
--- a/railties/lib/rails/tasks/framework.rake
+++ b/railties/lib/rails/tasks/framework.rake
@@ -9,8 +9,8 @@ namespace :app do
template = ENV["LOCATION"]
raise "No LOCATION value given. Please set LOCATION either as path to a file or a URL" if template.blank?
template = File.expand_path(template) if template !~ %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://}
- require_relative "../generators"
- require_relative "../generators/rails/app/app_generator"
+ require "rails/generators"
+ require "rails/generators/rails/app/app_generator"
generator = Rails::Generators::AppGenerator.new [Rails.root], {}, { destination_root: Rails.root }
generator.apply template, verbose: false
end
@@ -38,7 +38,7 @@ namespace :app do
end
namespace :update do
- require_relative "../app_updater"
+ require "rails/app_updater"
# desc "Update config/boot.rb from your current rails install"
task :configs do
diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake
deleted file mode 100644
index 403286d280..0000000000
--- a/railties/lib/rails/tasks/routes.rake
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require "optparse"
-
-desc "Print out all defined routes in match order, with names. Target specific controller with -c option, or grep routes using -g option"
-task routes: :environment do
- all_routes = Rails.application.routes.routes
- require "action_dispatch/routing/inspector"
- inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes)
-
- routes_filter = nil
-
- OptionParser.new do |opts|
- opts.banner = "Usage: rails routes [options]"
-
- Rake.application.standard_rake_options.each { |args| opts.on(*args) }
-
- opts.on("-c CONTROLLER") do |controller|
- routes_filter = { controller: controller }
- end
-
- opts.on("-g PATTERN") do |pattern|
- routes_filter = pattern
- end
-
- end.parse!(ARGV.reject { |x| x == "routes" })
-
- puts inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, routes_filter)
-
- exit 0 # ensure extra arguments aren't interpreted as Rake tasks
-end
diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake
index 0449a13586..594db91eec 100644
--- a/railties/lib/rails/tasks/statistics.rake
+++ b/railties/lib/rails/tasks/statistics.rake
@@ -26,6 +26,6 @@ end.select { |name, dir| File.directory?(dir) }
desc "Report code statistics (KLOCs, etc) from the application or engine"
task :stats do
- require_relative "../code_statistics"
+ require "rails/code_statistics"
CodeStatistics.new(*STATS_DIRECTORIES).to_s
end
diff --git a/railties/lib/rails/tasks/yarn.rake b/railties/lib/rails/tasks/yarn.rake
index 48da7ffc51..10703a1808 100644
--- a/railties/lib/rails/tasks/yarn.rake
+++ b/railties/lib/rails/tasks/yarn.rake
@@ -3,7 +3,7 @@
namespace :yarn do
desc "Install all JavaScript dependencies as specified via Yarn"
task :install do
- system("./bin/yarn install --no-progress --production")
+ system("./bin/yarn install --no-progress --frozen-lockfile --production")
end
end
diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb
index 89c1129f90..2a41c29602 100644
--- a/railties/lib/rails/templates/rails/mailers/email.html.erb
+++ b/railties/lib/rails/templates/rails/mailers/email.html.erb
@@ -95,11 +95,25 @@
</dd>
<% end %>
+ <dt>Format:</dt>
<% if @email.multipart? %>
<dd>
- <select onchange="formatChanged(this);">
- <option <%= request.format == Mime[:html] ? 'selected' : '' %> value="?<%= part_query('text/html') %>">View as HTML email</option>
- <option <%= request.format == Mime[:text] ? 'selected' : '' %> value="?<%= part_query('text/plain') %>">View as plain-text email</option>
+ <select id="part" onchange="refreshBody();">
+ <option <%= request.format == Mime[:html] ? 'selected' : '' %> value="<%= part_query('text/html') %>">View as HTML email</option>
+ <option <%= request.format == Mime[:text] ? 'selected' : '' %> value="<%= part_query('text/plain') %>">View as plain-text email</option>
+ </select>
+ </dd>
+ <% else %>
+ <dd id="mime_type" data-mime-type="<%= part_query(@email.mime_type) %>"><%= @email.mime_type == 'text/html' ? 'HTML email' : 'plain-text email' %></dd>
+ <% end %>
+
+ <% if I18n.available_locales.count > 1 %>
+ <dt>Locale:</dt>
+ <dd>
+ <select id="locale" onchange="refreshBody();">
+ <% I18n.available_locales.each do |locale| %>
+ <option <%= I18n.locale == locale ? 'selected' : '' %> value="<%= locale_query(locale) %>"><%= locale %></option>
+ <% end %>
</select>
</dd>
<% end %>
@@ -116,15 +130,27 @@
<% end %>
<script>
- function formatChanged(form) {
- var part_name = form.options[form.selectedIndex].value
- var iframe =document.getElementsByName('messageBody')[0];
- iframe.contentWindow.location.replace(part_name);
+ function refreshBody() {
+ var part_select = document.querySelector('select#part');
+ var locale_select = document.querySelector('select#locale');
+ var iframe = document.getElementsByName('messageBody')[0];
+ var part_param = part_select ?
+ part_select.options[part_select.selectedIndex].value :
+ document.querySelector('#mime_type').dataset.mimeType;
+ var locale_param = locale_select ? locale_select.options[locale_select.selectedIndex].value : null;
+ var fresh_location;
+ if (locale_param) {
+ fresh_location = '?' + part_param + '&' + locale_param;
+ } else {
+ fresh_location = '?' + part_param;
+ }
+ iframe.contentWindow.location = fresh_location;
if (history.replaceState) {
- var url = location.pathname.replace(/\.(txt|html)$/, '');
- var format = /html/.test(part_name) ? '.html' : '.txt';
- window.history.replaceState({}, '', url + format);
+ var url = location.pathname.replace(/\.(txt|html)$/, '');
+ var format = /html/.test(part_param) ? '.html' : '.txt';
+ var state_to_replace = locale_param ? (url + format + '?' + locale_param) : (url + format);
+ window.history.replaceState({}, '', state_to_replace);
}
}
</script>
diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb
index f755474488..4bd7d74b04 100644
--- a/railties/lib/rails/test_help.rb
+++ b/railties/lib/rails/test_help.rb
@@ -8,7 +8,7 @@ require "active_support/test_case"
require "action_controller"
require "action_controller/test_case"
require "action_dispatch/testing/integration"
-require_relative "generators/test_case"
+require "rails/generators/test_case"
require "active_support/testing/autorun"
@@ -22,6 +22,7 @@ if defined?(ActiveRecord::Base)
module ActiveSupport
class TestCase
+ include ActiveRecord::TestDatabases
include ActiveRecord::TestFixtures
self.fixture_path = "#{Rails.root}/test/fixtures/"
self.file_fixture_path = fixture_path + "files"
@@ -29,10 +30,6 @@ if defined?(ActiveRecord::Base)
end
ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
-
- def create_fixtures(*fixture_set_names, &block)
- FixtureSet.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, {}, &block)
- end
end
# :enddoc:
diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb
index 05f9a177d0..42b6daa3d1 100644
--- a/railties/lib/rails/test_unit/railtie.rb
+++ b/railties/lib/rails/test_unit/railtie.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "line_filtering"
+require "rails/test_unit/line_filtering"
if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^(default$|test(:|$))/).any?
ENV["RAILS_ENV"] ||= Rake.application.options.show_tasks ? "development" : "test"
diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb
index 7d3164f1eb..28b93cee5a 100644
--- a/railties/lib/rails/test_unit/reporter.rb
+++ b/railties/lib/rails/test_unit/reporter.rb
@@ -64,11 +64,17 @@ module Rails
end
def format_line(result)
- "%s#%s = %.2f s = %s" % [result.class, result.name, result.time, result.result_code]
+ klass = result.respond_to?(:klass) ? result.klass : result.class
+ "%s#%s = %.2f s = %s" % [klass, result.name, result.time, result.result_code]
end
def format_rerun_snippet(result)
- location, line = result.method(result.name).source_location
+ location, line = if result.respond_to?(:source_location)
+ result.source_location
+ else
+ result.method(result.name).source_location
+ end
+
"#{executable} #{relative_path_for(location)}:#{line}"
end
diff --git a/railties/lib/rails/test_unit/runner.rb b/railties/lib/rails/test_unit/runner.rb
index 5c2f6451e2..de5744c662 100644
--- a/railties/lib/rails/test_unit/runner.rb
+++ b/railties/lib/rails/test_unit/runner.rb
@@ -13,7 +13,7 @@ module Rails
class << self
def attach_before_load_options(opts)
opts.on("--warnings", "-w", "Run with Ruby warnings enabled") {}
- opts.on("--environment", "-e", "Run tests in the ENV environment") {}
+ opts.on("-e", "--environment ENV", "Run tests in the ENV environment") {}
end
def parse_options(argv)
diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake
index 18d699f806..32ac27a135 100644
--- a/railties/lib/rails/test_unit/testing.rake
+++ b/railties/lib/rails/test_unit/testing.rake
@@ -2,7 +2,7 @@
gem "minitest"
require "minitest"
-require_relative "runner"
+require "rails/test_unit/runner"
task default: :test
diff --git a/railties/lib/rails/welcome_controller.rb b/railties/lib/rails/welcome_controller.rb
index 0cb43e0f31..5b84b57679 100644
--- a/railties/lib/rails/welcome_controller.rb
+++ b/railties/lib/rails/welcome_controller.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "application_controller"
+require "rails/application_controller"
class Rails::WelcomeController < Rails::ApplicationController # :nodoc:
layout false
diff --git a/railties/railties.gemspec b/railties/railties.gemspec
index 4c665cd546..1df8b1fe39 100644
--- a/railties/railties.gemspec
+++ b/railties/railties.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Tools for creating, working with, and running Rails applications."
s.description = "Rails internals: application bootup, plugins, generators, and rake tasks."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/railties/test/app_loader_test.rb b/railties/test/app_loader_test.rb
index bb556f1968..c7a6bdee1b 100644
--- a/railties/test/app_loader_test.rb
+++ b/railties/test/app_loader_test.rb
@@ -40,13 +40,13 @@ class AppLoaderTest < ActiveSupport::TestCase
test "is not in a Rails application if #{exe} is not found in the current or parent directories" do
def loader.find_executables; end
- assert !loader.exec_app
+ assert_not loader.exec_app
end
test "is not in a Rails application if #{exe} exists but is a folder" do
FileUtils.mkdir_p(exe)
- assert !loader.exec_app
+ assert_not loader.exec_app
end
["APP_PATH", "ENGINE_PATH"].each do |keyword|
@@ -61,7 +61,7 @@ class AppLoaderTest < ActiveSupport::TestCase
test "is not in a Rails application if #{exe} exists but doesn't contain #{keyword}" do
write exe
- assert !loader.exec_app
+ assert_not loader.exec_app
end
test "is in a Rails application if parent directory has #{exe} containing #{keyword} and chdirs to the root directory" do
diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb
index e56c7b958e..3e0f31860b 100644
--- a/railties/test/application/asset_debugging_test.rb
+++ b/railties/test/application/asset_debugging_test.rb
@@ -77,7 +77,8 @@ module ApplicationTests
stylesheet_link_tag: %r{<link rel="stylesheet" media="screen" href="/stylesheets/#{contents}.css" />},
javascript_include_tag: %r{<script src="/javascripts/#{contents}.js">},
audio_tag: %r{<audio src="/audios/#{contents}"></audio>},
- video_tag: %r{<video src="/videos/#{contents}"></video>}
+ video_tag: %r{<video src="/videos/#{contents}"></video>},
+ image_submit_tag: %r{<input type="image" src="/images/#{contents}" />}
}
cases.each do |(view_method, tag_match)|
diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb
index 0d3262d6f6..9ef123c5b6 100644
--- a/railties/test/application/assets_test.rb
+++ b/railties/test/application/assets_test.rb
@@ -76,7 +76,7 @@ module ApplicationTests
# Load app env
app "production"
- assert !defined?(Uglifier)
+ assert_not defined?(Uglifier)
get "/assets/demo.js"
assert_match "alert()", last_response.body
assert defined?(Uglifier)
@@ -270,10 +270,10 @@ module ApplicationTests
app "production"
# Checking if Uglifier is defined we can know if Sprockets was reached or not
- assert !defined?(Uglifier)
+ assert_not defined?(Uglifier)
get "/assets/#{asset_path}"
assert_match "alert()", last_response.body
- assert !defined?(Uglifier)
+ assert_not defined?(Uglifier)
end
test "precompile properly refers files referenced with asset_path" do
diff --git a/railties/test/application/configuration/custom_test.rb b/railties/test/application/configuration/custom_test.rb
index 05b17b4a7a..608bc2fbe3 100644
--- a/railties/test/application/configuration/custom_test.rb
+++ b/railties/test/application/configuration/custom_test.rb
@@ -33,7 +33,7 @@ module ApplicationTests
assert_nil x.i_do_not_exist.zomg
# test that custom configuration responds to all messages
- assert_equal true, x.respond_to?(:i_do_not_exist)
+ assert_respond_to x, :i_do_not_exist
assert_kind_of Method, x.method(:i_do_not_exist)
assert_kind_of ActiveSupport::OrderedOptions, x.i_do_not_exist
end
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index c1a80eaeaf..84606d3b90 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -94,7 +94,7 @@ module ApplicationTests
with_rails_env "development" do
app "development"
- assert Rails.application.config.log_tags.blank?
+ assert_predicate Rails.application.config.log_tags, :blank?
end
end
@@ -361,7 +361,7 @@ module ApplicationTests
end
RUBY
- assert !$prepared
+ assert_not $prepared
app "development"
@@ -487,6 +487,32 @@ module ApplicationTests
assert_equal "some_value", Rails.application.message_verifier(:sensitive_value).verify(message)
end
+ test "config.secret_token is deprecated" do
+ app_file "config/initializers/secret_token.rb", <<-RUBY
+ Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
+ RUBY
+
+ app "production"
+
+ assert_deprecated(/secret_token/) do
+ app.secrets
+ end
+ end
+
+ test "secrets.secret_token is deprecated" do
+ app_file "config/secrets.yml", <<-YAML
+ production:
+ secret_token: "b3c631c314c0bbca50c1b2843150fe33"
+ YAML
+
+ app "production"
+
+ assert_deprecated(/secret_token/) do
+ app.secrets
+ end
+ end
+
+
test "raises when secret_key_base is blank" do
app_file "config/initializers/secret_token.rb", <<-RUBY
Rails.application.credentials.secret_key_base = nil
@@ -550,6 +576,7 @@ module ApplicationTests
app "development"
assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secrets.secret_key_base
+ assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secret_key_base
end
test "secret_key_base is copied from config to secrets when not set" do
@@ -681,6 +708,14 @@ module ApplicationTests
assert_match(/Missing.*RAILS_MASTER_KEY/, error)
end
+ test "credentials does not raise error when require_master_key is false and master key does not exist" do
+ remove_file "config/master.key"
+ add_to_config "config.require_master_key = false"
+ app "development"
+
+ assert_not app.credentials.secret_key_base
+ end
+
test "protect from forgery is the default in a new app" do
make_basic_app
@@ -731,6 +766,68 @@ module ApplicationTests
assert_match(/label/, last_response.body)
end
+ test "form_with can be configured with form_with_generates_ids" do
+ app_file "config/initializers/form_builder.rb", <<-RUBY
+ Rails.configuration.action_view.form_with_generates_ids = false
+ RUBY
+
+ app_file "app/models/post.rb", <<-RUBY
+ class Post
+ include ActiveModel::Model
+ attr_accessor :name
+ end
+ RUBY
+
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
+ class PostsController < ApplicationController
+ def index
+ render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>"
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ routes.prepend do
+ resources :posts
+ end
+ RUBY
+
+ app "development"
+
+ get "/posts"
+
+ assert_no_match(/id=('|")post_name('|")/, last_response.body)
+ end
+
+ test "form_with outputs ids by default" do
+ app_file "app/models/post.rb", <<-RUBY
+ class Post
+ include ActiveModel::Model
+ attr_accessor :name
+ end
+ RUBY
+
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
+ class PostsController < ApplicationController
+ def index
+ render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>"
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ routes.prepend do
+ resources :posts
+ end
+ RUBY
+
+ app "development"
+
+ get "/posts"
+
+ assert_match(/id=('|")post_name('|")/, last_response.body)
+ end
+
test "form_with can be configured with form_with_generates_remote_forms" do
app_file "config/initializers/form_builder.rb", <<-RUBY
Rails.configuration.action_view.form_with_generates_remote_forms = false
@@ -1229,7 +1326,7 @@ module ApplicationTests
assert_equal 200, last_response.status
end
- test "config.action_controller.action_on_unpermitted_parameters is :log by default on development" do
+ test "config.action_controller.action_on_unpermitted_parameters is :log by default in development" do
app "development"
force_lazy_load_hooks { ActionController::Base }
@@ -1238,7 +1335,7 @@ module ApplicationTests
assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters
end
- test "config.action_controller.action_on_unpermitted_parameters is :log by default on test" do
+ test "config.action_controller.action_on_unpermitted_parameters is :log by default in test" do
app "test"
force_lazy_load_hooks { ActionController::Base }
@@ -1247,7 +1344,7 @@ module ApplicationTests
assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters
end
- test "config.action_controller.action_on_unpermitted_parameters is false by default on production" do
+ test "config.action_controller.action_on_unpermitted_parameters is false by default in production" do
app "production"
force_lazy_load_hooks { ActionController::Base }
@@ -1320,14 +1417,14 @@ module ApplicationTests
assert_equal "XML", last_response.body
end
- test "Rails.application#env_config exists and include some existing parameters" do
+ test "Rails.application#env_config exists and includes some existing parameters" do
make_basic_app
- assert_equal app.env_config["action_dispatch.parameter_filter"], app.config.filter_parameters
- assert_equal app.env_config["action_dispatch.show_exceptions"], app.config.action_dispatch.show_exceptions
- assert_equal app.env_config["action_dispatch.logger"], Rails.logger
- assert_equal app.env_config["action_dispatch.backtrace_cleaner"], Rails.backtrace_cleaner
- assert_equal app.env_config["action_dispatch.key_generator"], Rails.application.key_generator
+ assert_equal app.env_config["action_dispatch.parameter_filter"], app.config.filter_parameters
+ assert_equal app.env_config["action_dispatch.show_exceptions"], app.config.action_dispatch.show_exceptions
+ assert_equal app.env_config["action_dispatch.logger"], Rails.logger
+ assert_equal app.env_config["action_dispatch.backtrace_cleaner"], Rails.backtrace_cleaner
+ assert_equal app.env_config["action_dispatch.key_generator"], Rails.application.key_generator
end
test "config.colorize_logging default is true" do
@@ -1382,7 +1479,7 @@ module ApplicationTests
test "respond_to? accepts include_private" do
make_basic_app
- assert_not Rails.configuration.respond_to?(:method_missing)
+ assert_not_respond_to Rails.configuration, :method_missing
assert Rails.configuration.respond_to?(:method_missing, true)
end
@@ -1394,12 +1491,18 @@ module ApplicationTests
assert_not ActiveRecord::Base.dump_schema_after_migration
end
- test "config.active_record.dump_schema_after_migration is true by default on development" do
+ test "config.active_record.dump_schema_after_migration is true by default in development" do
app "development"
assert ActiveRecord::Base.dump_schema_after_migration
end
+ test "config.active_record.verbose_query_logs is false by default in development" do
+ app "development"
+
+ assert_not ActiveRecord::Base.verbose_query_logs
+ end
+
test "config.annotations wrapping SourceAnnotationExtractor::Annotation class" do
make_basic_app do |application|
application.config.annotations.register_extensions("coffee") do |tag|
@@ -1407,7 +1510,7 @@ module ApplicationTests
end
end
- assert_not_nil SourceAnnotationExtractor::Annotation.extensions[/\.(coffee)$/]
+ assert_not_nil Rails::SourceAnnotationExtractor::Annotation.extensions[/\.(coffee)$/]
end
test "rake_tasks block works at instance level" do
@@ -1637,9 +1740,7 @@ module ApplicationTests
test "default SQLite3Adapter.represent_boolean_as_integer for 5.1 is false" do
remove_from_config '.*config\.load_defaults.*\n'
- add_to_top_of_config <<-RUBY
- config.load_defaults 5.1
- RUBY
+
app_file "app/models/post.rb", <<-RUBY
class Post < ActiveRecord::Base
end
@@ -1666,7 +1767,7 @@ module ApplicationTests
test "represent_boolean_as_integer should be able to set via config.active_record.sqlite3.represent_boolean_as_integer" do
remove_from_config '.*config\.load_defaults.*\n'
- app_file "config/initializers/new_framework_defaults_5_2.rb", <<-RUBY
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
RUBY
@@ -1731,7 +1832,7 @@ module ApplicationTests
test "api_only is false by default" do
app "development"
- refute Rails.application.config.api_only
+ assert_not Rails.application.config.api_only
end
test "api_only generator config is set when api_only is set" do
@@ -1788,6 +1889,96 @@ module ApplicationTests
assert_equal "https://example.org/", last_response.location
end
+ test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption is true by default for new apps" do
+ app "development"
+
+ assert_equal true, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption
+ end
+
+ test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption is false by default for upgraded apps" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app "development"
+
+ assert_equal false, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption
+ end
+
+ test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption can be configured via config.active_support.use_authenticated_message_encryption" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
+ Rails.application.config.active_support.use_authenticated_message_encryption = true
+ RUBY
+
+ app "development"
+
+ assert_equal true, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption
+ end
+
+ test "ActiveSupport::Digest.hash_digest_class is Digest::SHA1 by default for new apps" do
+ app "development"
+
+ assert_equal Digest::SHA1, ActiveSupport::Digest.hash_digest_class
+ end
+
+ test "ActiveSupport::Digest.hash_digest_class is Digest::MD5 by default for upgraded apps" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app "development"
+
+ assert_equal Digest::MD5, ActiveSupport::Digest.hash_digest_class
+ end
+
+ test "ActiveSupport::Digest.hash_digest_class can be configured via config.active_support.use_sha1_digests" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
+ Rails.application.config.active_support.use_sha1_digests = true
+ RUBY
+
+ app "development"
+
+ assert_equal Digest::SHA1, ActiveSupport::Digest.hash_digest_class
+ end
+
+ test "custom serializers should be able to set via config.active_job.custom_serializers in an initializer" do
+ class ::DummySerializer < ActiveJob::Serializers::ObjectSerializer; end
+
+ app_file "config/initializers/custom_serializers.rb", <<-RUBY
+ Rails.application.config.active_job.custom_serializers << DummySerializer
+ RUBY
+
+ app "development"
+
+ assert_includes ActiveJob::Serializers.serializers, DummySerializer
+ end
+
+ test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 is false by default" do
+ app "development"
+ assert_equal false, ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ end
+
+ test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 is true in an upgraded app" do
+ remove_from_config '.*config\.load_defaults.*\n'
+ add_to_config 'config.load_defaults "5.2"'
+
+ app "development"
+
+ assert_equal true, ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ end
+
+ test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 can be configured via config.action_view.default_enforce_utf8" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
+ Rails.application.config.action_view.default_enforce_utf8 = true
+ RUBY
+
+ app "development"
+
+ assert_equal true, ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ end
+
private
def force_lazy_load_hooks
yield # Tasty clarifying sugar, homie! We only need to reference a constant to load it.
diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb
index 13164f49c2..4a14042cd3 100644
--- a/railties/test/application/console_test.rb
+++ b/railties/test/application/console_test.rb
@@ -73,7 +73,7 @@ class ConsoleTest < ActiveSupport::TestCase
MODEL
load_environment
- assert User.new.respond_to?(:name)
+ assert_respond_to User.new, :name
app_file "app/models/user.rb", <<-MODEL
class User
@@ -81,9 +81,9 @@ class ConsoleTest < ActiveSupport::TestCase
end
MODEL
- assert !User.new.respond_to?(:age)
+ assert_not_respond_to User.new, :age
irb_context.reload!(false)
- assert User.new.respond_to?(:age)
+ assert_respond_to User.new, :age
end
def test_access_to_helpers
diff --git a/railties/test/application/content_security_policy_test.rb b/railties/test/application/content_security_policy_test.rb
new file mode 100644
index 0000000000..0d28df16f8
--- /dev/null
+++ b/railties/test/application/content_security_policy_test.rb
@@ -0,0 +1,223 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
+
+module ApplicationTests
+ class ContentSecurityPolicyTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+ include Rack::Test::Methods
+
+ def setup
+ build_app
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "default content security policy is nil" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_nil last_response.headers["Content-Security-Policy"]
+ end
+
+ test "empty content security policy is generated" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/content_security_policy.rb", <<-RUBY
+ Rails.application.config.content_security_policy do |p|
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy ""
+ end
+
+ test "global content security policy in an initializer" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/content_security_policy.rb", <<-RUBY
+ Rails.application.config.content_security_policy do |p|
+ p.default_src :self, :https
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "default-src 'self' https:"
+ end
+
+ test "global report only content security policy in an initializer" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/content_security_policy.rb", <<-RUBY
+ Rails.application.config.content_security_policy do |p|
+ p.default_src :self, :https
+ end
+
+ Rails.application.config.content_security_policy_report_only = true
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "default-src 'self' https:", report_only: true
+ end
+
+ test "override content security policy in a controller" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ content_security_policy do |p|
+ p.default_src "https://example.com"
+ end
+
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/content_security_policy.rb", <<-RUBY
+ Rails.application.config.content_security_policy do |p|
+ p.default_src :self, :https
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "default-src https://example.com"
+ end
+
+ test "override content security policy to report only in a controller" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ content_security_policy_report_only
+
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/content_security_policy.rb", <<-RUBY
+ Rails.application.config.content_security_policy do |p|
+ p.default_src :self, :https
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "default-src 'self' https:", report_only: true
+ end
+
+ test "global content security policy added to rack app" do
+ app_file "config/initializers/content_security_policy.rb", <<-RUBY
+ Rails.application.config.content_security_policy do |p|
+ p.default_src :self, :https
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+
+ app = ->(env) {
+ [200, { "Content-Type" => "text/html" }, ["<p>Hello, World!</p>"]]
+ }
+
+ root to: app
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "default-src 'self' https:"
+ end
+
+ private
+
+ def assert_policy(expected, report_only: false)
+ assert_equal 200, last_response.status
+
+ if report_only
+ expected_header = "Content-Security-Policy-Report-Only"
+ unexpected_header = "Content-Security-Policy"
+ else
+ expected_header = "Content-Security-Policy"
+ unexpected_header = "Content-Security-Policy-Report-Only"
+ end
+
+ assert_nil last_response.headers[unexpected_header]
+ assert_equal expected, last_response.headers[expected_header]
+ end
+ end
+end
diff --git a/railties/test/application/dbconsole_test.rb b/railties/test/application/dbconsole_test.rb
index ba04c81b4d..8eb293c179 100644
--- a/railties/test/application/dbconsole_test.rb
+++ b/railties/test/application/dbconsole_test.rb
@@ -19,21 +19,19 @@ module ApplicationTests
end
def test_use_value_defined_in_environment_file_in_database_yml
- Dir.chdir(app_path) do
- app_file "config/database.yml", <<-YAML
- development:
- database: <%= Rails.application.config.database %>
- adapter: sqlite3
- pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
- timeout: 5000
- YAML
+ app_file "config/database.yml", <<-YAML
+ development:
+ database: <%= Rails.application.config.database %>
+ adapter: sqlite3
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ timeout: 5000
+ YAML
- app_file "config/environments/development.rb", <<-RUBY
- Rails.application.configure do
- config.database = "db/development.sqlite3"
- end
- RUBY
- end
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.database = "db/development.sqlite3"
+ end
+ RUBY
master, slave = PTY.open
spawn_dbconsole(slave)
@@ -43,22 +41,20 @@ module ApplicationTests
end
def test_respect_environment_option
- Dir.chdir(app_path) do
- app_file "config/database.yml", <<-YAML
- default: &default
- adapter: sqlite3
- pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
- timeout: 5000
+ app_file "config/database.yml", <<-YAML
+ default: &default
+ adapter: sqlite3
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ timeout: 5000
- development:
- <<: *default
- database: db/development.sqlite3
+ development:
+ <<: *default
+ database: db/development.sqlite3
- production:
- <<: *default
- database: db/production.sqlite3
- YAML
- end
+ production:
+ <<: *default
+ database: db/production.sqlite3
+ YAML
master, slave = PTY.open
spawn_dbconsole(slave, "-e production")
diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb
index 47c815d221..e5e557d204 100644
--- a/railties/test/application/generators_test.rb
+++ b/railties/test/application/generators_test.rb
@@ -188,10 +188,11 @@ module ApplicationTests
Rails::Command.send(:remove_const, "APP_PATH")
end
- test "help does not show hidden namespaces" do
+ test "help does not show hidden namespaces and hidden commands" do
FileUtils.cd(rails_root) do
output = rails("generate", "--help")
assert_no_match "active_record:migration", output
+ assert_no_match "credentials", output
output = rails("destroy", "--help")
assert_no_match "active_record:migration", output
diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb
index d2b77bd015..1530ea82d6 100644
--- a/railties/test/application/initializers/frameworks_test.rb
+++ b/railties/test/application/initializers/frameworks_test.rb
@@ -226,7 +226,7 @@ module ApplicationTests
rails %w(generate model post title:string)
rails %w(db:migrate db:schema:cache:dump db:rollback)
require "#{app_path}/config/environment"
- assert !ActiveRecord::Base.connection.schema_cache.data_sources("posts")
+ assert_not ActiveRecord::Base.connection.schema_cache.data_sources("posts")
end
test "active record establish_connection uses Rails.env if DATABASE_URL is not set" do
@@ -266,7 +266,7 @@ module ApplicationTests
ActiveRecord::Base.connection
RUBY
require "#{app_path}/config/environment"
- assert !ActiveRecord::Base.connection_pool.active_connection?
+ assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
end
end
end
diff --git a/railties/test/application/initializers/notifications_test.rb b/railties/test/application/initializers/notifications_test.rb
index 23b20d578c..c65c955734 100644
--- a/railties/test/application/initializers/notifications_test.rb
+++ b/railties/test/application/initializers/notifications_test.rb
@@ -32,6 +32,7 @@ module ApplicationTests
logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
ActiveRecord::Base.logger = logger
+ ActiveRecord::Base.verbose_query_logs = false
# Mimic Active Record notifications
instrument "sql.active_record", name: "SQL", sql: "SHOW tables"
diff --git a/railties/test/application/integration_test_case_test.rb b/railties/test/application/integration_test_case_test.rb
index 9edc907fce..c08761092b 100644
--- a/railties/test/application/integration_test_case_test.rb
+++ b/railties/test/application/integration_test_case_test.rb
@@ -1,10 +1,11 @@
# frozen_string_literal: true
require "isolation/abstract_unit"
+require "env_helpers"
module ApplicationTests
class IntegrationTestCaseTest < ActiveSupport::TestCase
- include ActiveSupport::Testing::Isolation
+ include ActiveSupport::Testing::Isolation, EnvHelpers
setup do
build_app
@@ -39,13 +40,14 @@ module ApplicationTests
end
RUBY
+ with_rails_env("test") { rails("db:migrate") }
output = rails("test")
assert_match(/0 failures, 0 errors/, output)
end
end
class IntegrationTestDefaultApp < ActiveSupport::TestCase
- include ActiveSupport::Testing::Isolation
+ include ActiveSupport::Testing::Isolation, EnvHelpers
setup do
build_app
@@ -66,6 +68,7 @@ module ApplicationTests
end
RUBY
+ with_rails_env("test") { rails("db:migrate") }
output = rails("test")
assert_match(/0 failures, 0 errors/, output)
end
diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb
index 11c886e991..889ad16fb8 100644
--- a/railties/test/application/loading_test.rb
+++ b/railties/test/application/loading_test.rb
@@ -300,7 +300,7 @@ class LoadingTest < ActiveSupport::TestCase
end
MIGRATION
- Dir.chdir(app_path) { `rake db:migrate` }
+ rails("db:migrate")
require "#{rails_root}/config/environment"
get "/title"
@@ -314,7 +314,7 @@ class LoadingTest < ActiveSupport::TestCase
end
MIGRATION
- Dir.chdir(app_path) { `rake db:migrate` }
+ rails("db:migrate")
get "/body"
assert_equal "BODY", last_response.body
@@ -352,11 +352,23 @@ class LoadingTest < ActiveSupport::TestCase
def test_initialize_can_be_called_at_any_time
require "#{app_path}/config/application"
- assert !Rails.initialized?
- assert !Rails.application.initialized?
+ assert_not_predicate Rails, :initialized?
+ assert_not_predicate Rails.application, :initialized?
Rails.initialize!
- assert Rails.initialized?
- assert Rails.application.initialized?
+ assert_predicate Rails, :initialized?
+ assert_predicate Rails.application, :initialized?
+ end
+
+ test "frameworks aren't loaded during initialization" do
+ app_file "config/initializers/raise_when_frameworks_load.rb", <<-RUBY
+ %i(action_controller action_mailer active_job active_record).each do |framework|
+ ActiveSupport.on_load(framework) { raise "\#{framework} loaded!" }
+ end
+ RUBY
+
+ assert_nothing_raised do
+ require "#{app_path}/config/environment"
+ end
end
private
diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb
index 4e77cece1b..ba186bda44 100644
--- a/railties/test/application/mailer_previews_test.rb
+++ b/railties/test/application/mailer_previews_test.rb
@@ -296,7 +296,7 @@ module ApplicationTests
test "mailer preview not found" do
app("development")
get "/rails/mailers/notifier"
- assert last_response.not_found?
+ assert_predicate last_response, :not_found?
assert_match "Mailer preview &#39;notifier&#39; not found", last_response.body
end
@@ -326,7 +326,7 @@ module ApplicationTests
app("development")
get "/rails/mailers/notifier/bar"
- assert last_response.not_found?
+ assert_predicate last_response, :not_found?
assert_match "Email &#39;bar&#39; not found in NotifierPreview", last_response.body
end
@@ -382,7 +382,7 @@ module ApplicationTests
app("development")
get "/rails/mailers/notifier/foo?part=text%2Fhtml"
- assert last_response.not_found?
+ assert_predicate last_response, :not_found?
assert_match "Email part &#39;text/html&#39; not found in NotifierPreview#foo", last_response.body
end
@@ -450,11 +450,67 @@ module ApplicationTests
get "/rails/mailers/notifier/foo.html"
assert_equal 200, last_response.status
- assert_match '<option selected value="?part=text%2Fhtml">View as HTML email</option>', last_response.body
+ assert_match '<option selected value="part=text%2Fhtml">View as HTML email</option>', last_response.body
get "/rails/mailers/notifier/foo.txt"
assert_equal 200, last_response.status
- assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body
+ assert_match '<option selected value="part=text%2Fplain">View as plain-text email</option>', last_response.body
+ end
+
+ test "locale menu selects correct option" do
+ app_file "config/initializers/available_locales.rb", <<-RUBY
+ Rails.application.configure do
+ config.i18n.available_locales = %i[en ja]
+ end
+ RUBY
+
+ mailer "notifier", <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ html_template "notifier/foo", <<-RUBY
+ <p>Hello, World!</p>
+ RUBY
+
+ text_template "notifier/foo", <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview "notifier", <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app("development")
+
+ get "/rails/mailers/notifier/foo.html"
+ assert_equal 200, last_response.status
+ assert_match '<option selected value="locale=en">en', last_response.body
+ assert_match '<option value="locale=ja">ja', last_response.body
+
+ get "/rails/mailers/notifier/foo.html?locale=ja"
+ assert_equal 200, last_response.status
+ assert_match '<option value="locale=en">en', last_response.body
+ assert_match '<option selected value="locale=ja">ja', last_response.body
+
+ get "/rails/mailers/notifier/foo.txt"
+ assert_equal 200, last_response.status
+ assert_match '<option selected value="locale=en">en', last_response.body
+ assert_match '<option value="locale=ja">ja', last_response.body
+
+ get "/rails/mailers/notifier/foo.txt?locale=ja"
+ assert_equal 200, last_response.status
+ assert_match '<option value="locale=en">en', last_response.body
+ assert_match '<option selected value="locale=ja">ja', last_response.body
end
test "mailer previews create correct links when loaded on a subdirectory" do
@@ -520,8 +576,8 @@ module ApplicationTests
get "/rails/mailers/notifier/foo.txt"
assert_equal 200, last_response.status
assert_match '<iframe seamless name="messageBody" src="?part=text%2Fplain">', last_response.body
- assert_match '<option selected value="?part=text%2Fplain">', last_response.body
- assert_match '<option value="?part=text%2Fhtml">', last_response.body
+ assert_match '<option selected value="part=text%2Fplain">', last_response.body
+ assert_match '<option value="part=text%2Fhtml">', last_response.body
get "/rails/mailers/notifier/foo?part=text%2Fplain"
assert_equal 200, last_response.status
@@ -530,8 +586,8 @@ module ApplicationTests
get "/rails/mailers/notifier/foo.html?name=Ruby"
assert_equal 200, last_response.status
assert_match '<iframe seamless name="messageBody" src="?name=Ruby&amp;part=text%2Fhtml">', last_response.body
- assert_match '<option selected value="?name=Ruby&amp;part=text%2Fhtml">', last_response.body
- assert_match '<option value="?name=Ruby&amp;part=text%2Fplain">', last_response.body
+ assert_match '<option selected value="name=Ruby&amp;part=text%2Fhtml">', last_response.body
+ assert_match '<option value="name=Ruby&amp;part=text%2Fplain">', last_response.body
get "/rails/mailers/notifier/foo?name=Ruby&part=text%2Fhtml"
assert_equal 200, last_response.status
diff --git a/railties/test/application/middleware/cache_test.rb b/railties/test/application/middleware/cache_test.rb
index 9822ec563d..3768d8ce2d 100644
--- a/railties/test/application/middleware/cache_test.rb
+++ b/railties/test/application/middleware/cache_test.rb
@@ -140,7 +140,7 @@ module ApplicationTests
etag = last_response.headers["ETag"]
get "/expires/expires_etag", { private: true }, { "HTTP_IF_NONE_MATCH" => etag }
- assert_equal "miss", last_response.headers["X-Rack-Cache"]
+ assert_equal "miss", last_response.headers["X-Rack-Cache"]
assert_not_equal body, last_response.body
end
@@ -174,7 +174,7 @@ module ApplicationTests
last = last_response.headers["Last-Modified"]
get "/expires/expires_last_modified", { private: true }, { "HTTP_IF_MODIFIED_SINCE" => last }
- assert_equal "miss", last_response.headers["X-Rack-Cache"]
+ assert_equal "miss", last_response.headers["X-Rack-Cache"]
assert_not_equal body, last_response.body
end
end
diff --git a/railties/test/application/middleware/cookies_test.rb b/railties/test/application/middleware/cookies_test.rb
index 23f1ec3e35..ecb4ee3446 100644
--- a/railties/test/application/middleware/cookies_test.rb
+++ b/railties/test/application/middleware/cookies_test.rb
@@ -1,10 +1,12 @@
# frozen_string_literal: true
require "isolation/abstract_unit"
+require "rack/test"
module ApplicationTests
class CookiesTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
+ include Rack::Test::Methods
def new_app
File.expand_path("#{app_path}/../new_app")
@@ -15,6 +17,10 @@ module ApplicationTests
FileUtils.rm_rf("#{app_path}/config/environments")
end
+ def app
+ Rails.application
+ end
+
def teardown
teardown_app
FileUtils.rm_rf(new_app) if File.directory?(new_app)
@@ -44,5 +50,144 @@ module ApplicationTests
require "#{app_path}/config/environment"
assert_equal false, ActionDispatch::Cookies::CookieJar.always_write_cookie
end
+
+ test "signed cookies with SHA512 digest and rotated out SHA256 and SHA1 digests" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get ':controller(/:action)'
+ post ':controller(/:action)'
+ end
+ RUBY
+
+ controller :foo, <<-RUBY
+ class FooController < ActionController::Base
+ protect_from_forgery with: :null_session
+
+ def write_raw_cookie_sha1
+ cookies[:signed_cookie] = TestVerifiers.sha1.generate("signed cookie")
+ head :ok
+ end
+
+ def write_raw_cookie_sha256
+ cookies[:signed_cookie] = TestVerifiers.sha256.generate("signed cookie")
+ head :ok
+ end
+
+ def read_signed
+ render plain: cookies.signed[:signed_cookie].inspect
+ end
+
+ def read_raw_cookie
+ render plain: cookies[:signed_cookie]
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ sha1_secret = Rails.application.key_generator.generate_key("sha1")
+ sha256_secret = Rails.application.key_generator.generate_key("sha256")
+
+ ::TestVerifiers = Class.new do
+ class_attribute :sha1, default: ActiveSupport::MessageVerifier.new(sha1_secret, digest: "SHA1")
+ class_attribute :sha256, default: ActiveSupport::MessageVerifier.new(sha256_secret, digest: "SHA256")
+ end
+
+ config.action_dispatch.signed_cookie_digest = "SHA512"
+ config.action_dispatch.signed_cookie_salt = "sha512 salt"
+
+ config.action_dispatch.cookies_rotations.tap do |cookies|
+ cookies.rotate :signed, sha1_secret, digest: "SHA1"
+ cookies.rotate :signed, sha256_secret, digest: "SHA256"
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ verifier_sha512 = ActiveSupport::MessageVerifier.new(app.key_generator.generate_key("sha512 salt"), digest: :SHA512)
+
+ get "/foo/write_raw_cookie_sha1"
+ get "/foo/read_signed"
+ assert_equal "signed cookie".inspect, last_response.body
+
+ get "/foo/read_raw_cookie"
+ assert_equal "signed cookie", verifier_sha512.verify(last_response.body)
+
+ get "/foo/write_raw_cookie_sha256"
+ get "/foo/read_signed"
+ assert_equal "signed cookie".inspect, last_response.body
+
+ get "/foo/read_raw_cookie"
+ assert_equal "signed cookie", verifier_sha512.verify(last_response.body)
+ end
+
+ test "encrypted cookies rotating multiple encryption keys" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get ':controller(/:action)'
+ post ':controller(/:action)'
+ end
+ RUBY
+
+ controller :foo, <<-RUBY
+ class FooController < ActionController::Base
+ protect_from_forgery with: :null_session
+
+ def write_raw_cookie_one
+ cookies[:encrypted_cookie] = TestEncryptors.first_gcm.encrypt_and_sign("encrypted cookie")
+ head :ok
+ end
+
+ def write_raw_cookie_two
+ cookies[:encrypted_cookie] = TestEncryptors.second_gcm.encrypt_and_sign("encrypted cookie")
+ head :ok
+ end
+
+ def read_encrypted
+ render plain: cookies.encrypted[:encrypted_cookie].inspect
+ end
+
+ def read_raw_cookie
+ render plain: cookies[:encrypted_cookie]
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ first_secret = Rails.application.key_generator.generate_key("first", 32)
+ second_secret = Rails.application.key_generator.generate_key("second", 32)
+
+ ::TestEncryptors = Class.new do
+ class_attribute :first_gcm, default: ActiveSupport::MessageEncryptor.new(first_secret, cipher: "aes-256-gcm")
+ class_attribute :second_gcm, default: ActiveSupport::MessageEncryptor.new(second_secret, cipher: "aes-256-gcm")
+ end
+
+ config.action_dispatch.use_authenticated_cookie_encryption = true
+ config.action_dispatch.encrypted_cookie_cipher = "aes-256-gcm"
+ config.action_dispatch.authenticated_encrypted_cookie_salt = "salt"
+
+ config.action_dispatch.cookies_rotations.tap do |cookies|
+ cookies.rotate :encrypted, first_secret
+ cookies.rotate :encrypted, second_secret
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ encryptor = ActiveSupport::MessageEncryptor.new(app.key_generator.generate_key("salt", 32), cipher: "aes-256-gcm")
+
+ get "/foo/write_raw_cookie_one"
+ get "/foo/read_encrypted"
+ assert_equal "encrypted cookie".inspect, last_response.body
+
+ get "/foo/read_raw_cookie"
+ assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body)
+
+ get "/foo/write_raw_cookie_sha256"
+ get "/foo/read_encrypted"
+ assert_equal "encrypted cookie".inspect, last_response.body
+
+ get "/foo/read_raw_cookie"
+ assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body)
+ end
end
end
diff --git a/railties/test/application/middleware/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb
index 75afeec905..2d659ade8d 100644
--- a/railties/test/application/middleware/exceptions_test.rb
+++ b/railties/test/application/middleware/exceptions_test.rb
@@ -102,7 +102,7 @@ module ApplicationTests
end
end
- test "routing to an nonexistent controller when action_dispatch.show_exceptions and consider_all_requests_local are set shows diagnostics" do
+ test "routing to a nonexistent controller when action_dispatch.show_exceptions and consider_all_requests_local are set shows diagnostics" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
resources :articles
diff --git a/railties/test/application/middleware/sendfile_test.rb b/railties/test/application/middleware/sendfile_test.rb
index 9def3a0ce7..818ad61c64 100644
--- a/railties/test/application/middleware/sendfile_test.rb
+++ b/railties/test/application/middleware/sendfile_test.rb
@@ -29,7 +29,7 @@ module ApplicationTests
simple_controller
get "/"
- assert !last_response.headers["X-Sendfile"]
+ assert_not last_response.headers["X-Sendfile"]
assert_equal File.read(__FILE__), last_response.body
end
diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb
index a17988235a..9182a63ab7 100644
--- a/railties/test/application/middleware/session_test.rb
+++ b/railties/test/application/middleware/session_test.rb
@@ -31,7 +31,7 @@ module ApplicationTests
add_to_config "config.force_ssl = true"
add_to_config "config.ssl_options = { secure_cookies: false }"
require "#{app_path}/config/environment"
- assert !app.config.session_options[:secure]
+ assert_not app.config.session_options[:secure]
end
test "session is not loaded if it's not used" do
@@ -51,7 +51,7 @@ module ApplicationTests
get "/"
assert last_request.env["HTTP_COOKIE"]
- assert !last_response.headers["Set-Cookie"]
+ assert_not last_response.headers["Set-Cookie"]
end
test "session is empty and isn't saved on unverified request when using :null_session protect method" do
diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb
index 0a5a524692..5efaf841d4 100644
--- a/railties/test/application/middleware_test.rb
+++ b/railties/test/application/middleware_test.rb
@@ -42,9 +42,11 @@ module ApplicationTests
"ActionDispatch::Cookies",
"ActionDispatch::Session::CookieStore",
"ActionDispatch::Flash",
+ "ActionDispatch::ContentSecurityPolicy::Middleware",
"Rack::Head",
"Rack::ConditionalGet",
- "Rack::ETag"
+ "Rack::ETag",
+ "Rack::TempfileReaper"
], middleware
end
@@ -248,7 +250,7 @@ module ApplicationTests
test "can't change middleware after it's built" do
boot!
- assert_raise RuntimeError do
+ assert_raise frozen_error_class do
app.config.middleware.use Rack::Config
end
end
diff --git a/railties/test/application/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb
index e9bc91785c..10d3313f6e 100644
--- a/railties/test/application/per_request_digest_cache_test.rb
+++ b/railties/test/application/per_request_digest_cache_test.rb
@@ -59,7 +59,7 @@ class PerRequestDigestCacheTest < ActiveSupport::TestCase
assert_equal 200, last_response.status
values = ActionView::LookupContext::DetailsKey.digest_caches.first.values
- assert_equal [ "8ba099b7749542fe765ff34a6824d548" ], values
+ assert_equal [ "effc8928d0b33535c8a21d24ec617161" ], values
assert_equal %w(david dingus), last_response.body.split.map(&:strip)
end
diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb
index 391676aa31..0594236b1f 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -26,15 +26,15 @@ module ApplicationTests
FileUtils.rm_rf("#{app_path}/config/database.yml")
end
- def db_create_and_drop(expected_database)
+ def db_create_and_drop(expected_database, environment_loaded: true)
Dir.chdir(app_path) do
output = rails("db:create")
assert_match(/Created database/, output)
assert File.exist?(expected_database)
- assert_equal expected_database, ActiveRecord::Base.connection_config[:database]
+ assert_equal expected_database, ActiveRecord::Base.connection_config[:database] if environment_loaded
output = rails("db:drop")
assert_match(/Dropped database/, output)
- assert !File.exist?(expected_database)
+ assert_not File.exist?(expected_database)
end
end
@@ -49,6 +49,22 @@ module ApplicationTests
db_create_and_drop database_url_db_name
end
+ test "db:create and db:drop respect environment setting" do
+ app_file "config/database.yml", <<-YAML
+ development:
+ database: <%= Rails.application.config.database %>
+ adapter: sqlite3
+ YAML
+
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.database = "db/development.sqlite3"
+ end
+ RUBY
+
+ db_create_and_drop "db/development.sqlite3", environment_loaded: false
+ end
+
def with_database_existing
Dir.chdir(app_path) do
set_database_url
@@ -82,13 +98,25 @@ module ApplicationTests
end
end
- test "db:drop failure because database does not exist" do
- Dir.chdir(app_path) do
- output = rails("db:drop:_unsafe", "--trace")
- assert_match(/does not exist/, output)
+ test "db:create works when schema cache exists and database does not exist" do
+ use_postgresql
+
+ begin
+ rails %w(db:create db:migrate db:schema:cache:dump)
+
+ rails "db:drop"
+ rails "db:create"
+ assert_equal 0, $?.exitstatus
+ ensure
+ rails "db:drop" rescue nil
end
end
+ test "db:drop failure because database does not exist" do
+ output = rails("db:drop:_unsafe", "--trace")
+ assert_match(/does not exist/, output)
+ end
+
test "db:drop failure because bad permissions" do
with_database_existing do
with_bad_permissions do
@@ -100,13 +128,11 @@ module ApplicationTests
end
def db_migrate_and_status(expected_database)
- Dir.chdir(app_path) do
- rails "generate", "model", "book", "title:string"
- rails "db:migrate"
- output = rails("db:migrate:status")
- assert_match(%r{database:\s+\S*#{Regexp.escape(expected_database)}}, output)
- assert_match(/up\s+\d{14}\s+Create books/, output)
- end
+ rails "generate", "model", "book", "title:string"
+ rails "db:migrate"
+ output = rails("db:migrate:status")
+ assert_match(%r{database:\s+\S*#{Regexp.escape(expected_database)}}, output)
+ assert_match(/up\s+\d{14}\s+Create books/, output)
end
test "db:migrate and db:migrate:status without database_url" do
@@ -161,12 +187,11 @@ module ApplicationTests
test "db:fixtures:load with namespaced fixture" do
require "#{app_path}/config/environment"
- Dir.chdir(app_path) do
- rails "generate", "model", "admin::book", "title:string"
- rails "db:migrate", "db:fixtures:load"
- require "#{app_path}/app/models/admin/book"
- assert_equal 2, Admin::Book.count
- end
+
+ rails "generate", "model", "admin::book", "title:string"
+ rails "db:migrate", "db:fixtures:load"
+ require "#{app_path}/app/models/admin/book"
+ assert_equal 2, Admin::Book.count
end
def db_structure_dump_and_load(expected_database)
@@ -178,7 +203,7 @@ module ApplicationTests
rails "environment", "db:drop", "db:structure:load"
assert_match expected_database, ActiveRecord::Base.connection_config[:database]
require "#{app_path}/app/models/book"
- #if structure is not loaded correctly, exception would be raised
+ # if structure is not loaded correctly, exception would be raised
assert_equal 0, Book.count
end
end
@@ -194,6 +219,14 @@ module ApplicationTests
db_structure_dump_and_load database_url_db_name
end
+ test "db:structure:dump and db:structure:load set ar_internal_metadata" do
+ require "#{app_path}/config/environment"
+ db_structure_dump_and_load ActiveRecord::Base.configurations[Rails.env]["database"]
+
+ assert_equal "test", rails("runner", "-e", "test", "puts ActiveRecord::InternalMetadata[:environment]").strip
+ assert_equal "development", rails("runner", "puts ActiveRecord::InternalMetadata[:environment]").strip
+ end
+
test "db:structure:dump does not dump schema information when no migrations are used" do
# create table without migrations
rails "runner", "ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }"
@@ -205,56 +238,52 @@ module ApplicationTests
end
test "db:schema:load and db:structure:load do not purge the existing database" do
- Dir.chdir(app_path) do
- rails "runner", "ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }"
+ rails "runner", "ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }"
- app_file "db/schema.rb", <<-RUBY
- ActiveRecord::Schema.define(version: 20140423102712) do
- create_table(:comments) {}
- end
- RUBY
+ app_file "db/schema.rb", <<-RUBY
+ ActiveRecord::Schema.define(version: 20140423102712) do
+ create_table(:comments) {}
+ end
+ RUBY
- list_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip }
+ list_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip }
- assert_equal '["posts"]', list_tables[]
- rails "db:schema:load"
- assert_equal '["posts", "comments", "schema_migrations", "ar_internal_metadata"]', list_tables[]
+ assert_equal '["posts"]', list_tables[]
+ rails "db:schema:load"
+ assert_equal '["posts", "comments", "schema_migrations", "ar_internal_metadata"]', list_tables[]
- app_file "db/structure.sql", <<-SQL
- CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255));
- SQL
+ app_file "db/structure.sql", <<-SQL
+ CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255));
+ SQL
- rails "db:structure:load"
- assert_equal '["posts", "comments", "schema_migrations", "ar_internal_metadata", "users"]', list_tables[]
- end
+ rails "db:structure:load"
+ assert_equal '["posts", "comments", "schema_migrations", "ar_internal_metadata", "users"]', list_tables[]
end
test "db:schema:load with inflections" do
- Dir.chdir(app_path) do
- app_file "config/initializers/inflection.rb", <<-RUBY
- ActiveSupport::Inflector.inflections do |inflect|
- inflect.irregular 'goose', 'geese'
- end
- RUBY
- app_file "config/initializers/primary_key_table_name.rb", <<-RUBY
- ActiveRecord::Base.primary_key_prefix_type = :table_name
- RUBY
- app_file "db/schema.rb", <<-RUBY
- ActiveRecord::Schema.define(version: 20140423102712) do
- create_table("goose".pluralize) do |t|
- t.string :name
- end
+ app_file "config/initializers/inflection.rb", <<-RUBY
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular 'goose', 'geese'
+ end
+ RUBY
+ app_file "config/initializers/primary_key_table_name.rb", <<-RUBY
+ ActiveRecord::Base.primary_key_prefix_type = :table_name
+ RUBY
+ app_file "db/schema.rb", <<-RUBY
+ ActiveRecord::Schema.define(version: 20140423102712) do
+ create_table("goose".pluralize) do |t|
+ t.string :name
end
- RUBY
+ end
+ RUBY
- rails "db:schema:load"
+ rails "db:schema:load"
- tables = rails("runner", "p ActiveRecord::Base.connection.tables").strip
- assert_match(/"geese"/, tables)
+ tables = rails("runner", "p ActiveRecord::Base.connection.tables").strip
+ assert_match(/"geese"/, tables)
- columns = rails("runner", "p ActiveRecord::Base.connection.columns('geese').map(&:name)").strip
- assert_equal columns, '["gooseid", "name"]'
- end
+ columns = rails("runner", "p ActiveRecord::Base.connection.columns('geese').map(&:name)").strip
+ assert_equal columns, '["gooseid", "name"]'
end
test "db:schema:load fails if schema.rb doesn't exist yet" do
@@ -269,7 +298,7 @@ module ApplicationTests
ActiveRecord::Base.configurations = Rails.application.config.database_configuration
ActiveRecord::Base.establish_connection :test
require "#{app_path}/app/models/book"
- #if structure is not loaded correctly, exception would be raised
+ # if structure is not loaded correctly, exception would be raised
assert_equal 0, Book.count
assert_match ActiveRecord::Base.configurations["test"]["database"],
ActiveRecord::Base.connection_config[:database]
@@ -300,15 +329,52 @@ module ApplicationTests
puts ActiveRecord::Base.connection_config[:database]
RUBY
- Dir.chdir(app_path) do
- database_path = rails("db:setup")
- assert_equal "development.sqlite3", File.basename(database_path.strip)
- end
+ database_path = rails("db:setup")
+ assert_equal "development.sqlite3", File.basename(database_path.strip)
ensure
ENV["RAILS_ENV"] = @old_rails_env
ENV["RACK_ENV"] = @old_rack_env
end
end
+
+ test "db:setup sets ar_internal_metadata" do
+ app_file "db/schema.rb", ""
+ rails "db:setup"
+
+ test_environment = lambda { rails("runner", "-e", "test", "puts ActiveRecord::InternalMetadata[:environment]").strip }
+ development_environment = lambda { rails("runner", "puts ActiveRecord::InternalMetadata[:environment]").strip }
+
+ assert_equal "test", test_environment.call
+ assert_equal "development", development_environment.call
+
+ app_file "db/structure.sql", ""
+ app_file "config/initializers/enable_sql_schema_format.rb", <<-RUBY
+ Rails.application.config.active_record.schema_format = :sql
+ RUBY
+
+ rails "db:setup"
+
+ assert_equal "test", test_environment.call
+ assert_equal "development", development_environment.call
+ end
+
+ test "db:test:prepare sets test ar_internal_metadata" do
+ app_file "db/schema.rb", ""
+ rails "db:test:prepare"
+
+ test_environment = lambda { rails("runner", "-e", "test", "puts ActiveRecord::InternalMetadata[:environment]").strip }
+
+ assert_equal "test", test_environment.call
+
+ app_file "db/structure.sql", ""
+ app_file "config/initializers/enable_sql_schema_format.rb", <<-RUBY
+ Rails.application.config.active_record.schema_format = :sql
+ RUBY
+
+ rails "db:test:prepare"
+
+ assert_equal "test", test_environment.call
+ end
end
end
end
diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb
index b0d51eb22e..bf5b07afbd 100644
--- a/railties/test/application/rake/migrations_test.rb
+++ b/railties/test/application/rake/migrations_test.rb
@@ -15,69 +15,166 @@ module ApplicationTests
end
test "running migrations with given scope" do
- Dir.chdir(app_path) do
- rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "model", "user", "username:string", "password:string"
- app_file "db/migrate/01_a_migration.bukkits.rb", <<-MIGRATION
- class AMigration < ActiveRecord::Migration::Current
- end
- MIGRATION
+ app_file "db/migrate/01_a_migration.bukkits.rb", <<-MIGRATION
+ class AMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
- output = rails("db:migrate", "SCOPE=bukkits")
- assert_no_match(/create_table\(:users\)/, output)
- assert_no_match(/CreateUsers/, output)
- assert_no_match(/add_column\(:users, :email, :string\)/, output)
+ output = rails("db:migrate", "SCOPE=bukkits")
+ assert_no_match(/create_table\(:users\)/, output)
+ assert_no_match(/CreateUsers/, output)
+ assert_no_match(/add_column\(:users, :email, :string\)/, output)
- assert_match(/AMigration: migrated/, output)
+ assert_match(/AMigration: migrated/, output)
- output = rails("db:migrate", "SCOPE=bukkits", "VERSION=0")
- assert_no_match(/drop_table\(:users\)/, output)
- assert_no_match(/CreateUsers/, output)
- assert_no_match(/remove_column\(:users, :email\)/, output)
+ # run all the migrations to test scope for down
+ output = rails("db:migrate")
+ assert_match(/CreateUsers: migrated/, output)
- assert_match(/AMigration: reverted/, output)
- end
+ output = rails("db:migrate", "SCOPE=bukkits", "VERSION=0")
+ assert_no_match(/drop_table\(:users\)/, output)
+ assert_no_match(/CreateUsers/, output)
+ assert_no_match(/remove_column\(:users, :email\)/, output)
+
+ assert_match(/AMigration: reverted/, output)
+
+ output = rails("db:migrate", "VERSION=0")
+
+ assert_match(/CreateUsers: reverted/, output)
+ end
+
+ test "version outputs current version" do
+ app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ rails "db:migrate"
+
+ output = rails("db:version")
+ assert_match(/Current version: 1/, output)
+ end
+
+ test "migrate with specified VERSION in different formats" do
+ app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ app_file "db/migrate/02_two_migration.rb", <<-MIGRATION
+ class TwoMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ app_file "db/migrate/03_three_migration.rb", <<-MIGRATION
+ class ThreeMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ rails "db:migrate"
+
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
+ assert_match(/up\s+003\s+Three migration/, output)
+
+ rails "db:migrate", "VERSION=01_one_migration.rb"
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/down\s+002\s+Two migration/, output)
+ assert_match(/down\s+003\s+Three migration/, output)
+
+ rails "db:migrate", "VERSION=3"
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
+ assert_match(/up\s+003\s+Three migration/, output)
+
+ rails "db:migrate", "VERSION=001"
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/down\s+002\s+Two migration/, output)
+ assert_match(/down\s+003\s+Three migration/, output)
end
test "migration with empty version" do
- Dir.chdir(app_path) do
- output = rails("db:migrate", "VERSION=", allow_failure: true)
- assert_match(/Empty VERSION provided/, output)
+ app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
- output = rails("db:migrate:redo", "VERSION=", allow_failure: true)
- assert_match(/Empty VERSION provided/, output)
+ app_file "db/migrate/02_two_migration.rb", <<-MIGRATION
+ class TwoMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
- output = rails("db:migrate:up", "VERSION=", allow_failure: true)
- assert_match(/VERSION is required/, output)
+ rails("db:migrate", "VERSION=")
- output = rails("db:migrate:up", allow_failure: true)
- assert_match(/VERSION is required/, output)
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
- output = rails("db:migrate:down", "VERSION=", allow_failure: true)
- assert_match(/VERSION is required - To go down one migration, use db:rollback/, output)
+ output = rails("db:migrate:redo", "VERSION=", allow_failure: true)
+ assert_match(/Empty VERSION provided/, output)
- output = rails("db:migrate:down", allow_failure: true)
- assert_match(/VERSION is required - To go down one migration, use db:rollback/, output)
- end
+ output = rails("db:migrate:up", "VERSION=", allow_failure: true)
+ assert_match(/VERSION is required/, output)
+
+ output = rails("db:migrate:up", allow_failure: true)
+ assert_match(/VERSION is required/, output)
+
+ output = rails("db:migrate:down", "VERSION=", allow_failure: true)
+ assert_match(/VERSION is required - To go down one migration, use db:rollback/, output)
+
+ output = rails("db:migrate:down", allow_failure: true)
+ assert_match(/VERSION is required - To go down one migration, use db:rollback/, output)
+
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
end
- test "model and migration generator with change syntax" do
- Dir.chdir(app_path) do
- rails "generate", "model", "user", "username:string", "password:string"
- rails "generate", "migration", "add_email_to_users", "email:string"
+ test "migration with 0 version" do
+ app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
- output = rails("db:migrate")
- assert_match(/create_table\(:users\)/, output)
- assert_match(/CreateUsers: migrated/, output)
- assert_match(/add_column\(:users, :email, :string\)/, output)
- assert_match(/AddEmailToUsers: migrated/, output)
-
- output = rails("db:rollback", "STEP=2")
- assert_match(/drop_table\(:users\)/, output)
- assert_match(/CreateUsers: reverted/, output)
- assert_match(/remove_column\(:users, :email, :string\)/, output)
- assert_match(/AddEmailToUsers: reverted/, output)
- end
+ app_file "db/migrate/02_two_migration.rb", <<-MIGRATION
+ class TwoMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ rails "db:migrate"
+
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
+
+ rails "db:migrate", "VERSION=0"
+
+ output = rails("db:migrate:status")
+ assert_match(/down\s+001\s+One migration/, output)
+ assert_match(/down\s+002\s+Two migration/, output)
+ end
+
+ test "model and migration generator with change syntax" do
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+
+ output = rails("db:migrate")
+ assert_match(/create_table\(:users\)/, output)
+ assert_match(/CreateUsers: migrated/, output)
+ assert_match(/add_column\(:users, :email, :string\)/, output)
+ assert_match(/AddEmailToUsers: migrated/, output)
+
+ output = rails("db:rollback", "STEP=2")
+ assert_match(/drop_table\(:users\)/, output)
+ assert_match(/CreateUsers: reverted/, output)
+ assert_match(/remove_column\(:users, :email, :string\)/, output)
+ assert_match(/AddEmailToUsers: reverted/, output)
end
test "migration status when schema migrations table is not present" do
@@ -86,93 +183,85 @@ module ApplicationTests
end
test "migration status" do
- Dir.chdir(app_path) do
- rails "generate", "model", "user", "username:string", "password:string"
- rails "generate", "migration", "add_email_to_users", "email:string"
- rails "db:migrate"
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+ rails "db:migrate"
- output = rails("db:migrate:status")
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{14}\s+Create users/, output)
- assert_match(/up\s+\d{14}\s+Add email to users/, output)
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+Add email to users/, output)
- rails "db:rollback", "STEP=1"
- output = rails("db:migrate:status")
+ rails "db:rollback", "STEP=1"
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{14}\s+Create users/, output)
- assert_match(/down\s+\d{14}\s+Add email to users/, output)
- end
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/down\s+\d{14}\s+Add email to users/, output)
end
test "migration status without timestamps" do
add_to_config("config.active_record.timestamped_migrations = false")
- Dir.chdir(app_path) do
- rails "generate", "model", "user", "username:string", "password:string"
- rails "generate", "migration", "add_email_to_users", "email:string"
- rails "db:migrate"
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+ rails "db:migrate"
- output = rails("db:migrate:status")
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{3,}\s+Create users/, output)
- assert_match(/up\s+\d{3,}\s+Add email to users/, output)
+ assert_match(/up\s+\d{3,}\s+Create users/, output)
+ assert_match(/up\s+\d{3,}\s+Add email to users/, output)
- rails "db:rollback", "STEP=1"
- output = rails("db:migrate:status")
+ rails "db:rollback", "STEP=1"
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{3,}\s+Create users/, output)
- assert_match(/down\s+\d{3,}\s+Add email to users/, output)
- end
+ assert_match(/up\s+\d{3,}\s+Create users/, output)
+ assert_match(/down\s+\d{3,}\s+Add email to users/, output)
end
test "migration status after rollback and redo" do
- Dir.chdir(app_path) do
- rails "generate", "model", "user", "username:string", "password:string"
- rails "generate", "migration", "add_email_to_users", "email:string"
- rails "db:migrate"
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+ rails "db:migrate"
- output = rails("db:migrate:status")
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{14}\s+Create users/, output)
- assert_match(/up\s+\d{14}\s+Add email to users/, output)
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+Add email to users/, output)
- rails "db:rollback", "STEP=2"
- output = rails("db:migrate:status")
+ rails "db:rollback", "STEP=2"
+ output = rails("db:migrate:status")
- assert_match(/down\s+\d{14}\s+Create users/, output)
- assert_match(/down\s+\d{14}\s+Add email to users/, output)
+ assert_match(/down\s+\d{14}\s+Create users/, output)
+ assert_match(/down\s+\d{14}\s+Add email to users/, output)
- rails "db:migrate:redo"
- output = rails("db:migrate:status")
+ rails "db:migrate:redo"
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{14}\s+Create users/, output)
- assert_match(/up\s+\d{14}\s+Add email to users/, output)
- end
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+Add email to users/, output)
end
test "migration status after rollback and forward" do
- Dir.chdir(app_path) do
- rails "generate", "model", "user", "username:string", "password:string"
- rails "generate", "migration", "add_email_to_users", "email:string"
- rails "db:migrate"
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+ rails "db:migrate"
- output = rails("db:migrate:status")
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{14}\s+Create users/, output)
- assert_match(/up\s+\d{14}\s+Add email to users/, output)
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+Add email to users/, output)
- rails "db:rollback", "STEP=2"
- output = rails("db:migrate:status")
+ rails "db:rollback", "STEP=2"
+ output = rails("db:migrate:status")
- assert_match(/down\s+\d{14}\s+Create users/, output)
- assert_match(/down\s+\d{14}\s+Add email to users/, output)
+ assert_match(/down\s+\d{14}\s+Create users/, output)
+ assert_match(/down\s+\d{14}\s+Add email to users/, output)
- rails "db:forward", "STEP=2"
- output = rails("db:migrate:status")
+ rails "db:forward", "STEP=2"
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{14}\s+Create users/, output)
- assert_match(/up\s+\d{14}\s+Add email to users/, output)
- end
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+Add email to users/, output)
end
test "raise error on any move when current migration does not exist" do
@@ -206,53 +295,117 @@ module ApplicationTests
end
end
+ test "raise error on any move when target migration does not exist" do
+ app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ app_file "db/migrate/02_two_migration.rb", <<-MIGRATION
+ class TwoMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ rails "db:migrate"
+
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
+
+ output = rails("db:migrate", "VERSION=3", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/ActiveRecord::UnknownMigrationVersionError:/, output)
+ assert_match(/No migration with version number 3/, output)
+
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
+ end
+
+ test "raise error on any move when VERSION has invalid format" do
+ output = rails("db:migrate", "VERSION=unknown", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
+
+ output = rails("db:migrate", "VERSION=0.1.11", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
+
+ output = rails("db:migrate", "VERSION=1.1.11", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
+
+ output = rails("db:migrate", "VERSION='0 '", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
+
+ output = rails("db:migrate", "VERSION=1.", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
+
+ output = rails("db:migrate", "VERSION=1_", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
+
+ output = rails("db:migrate", "VERSION=1_name", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
+
+ output = rails("db:migrate:redo", "VERSION=unknown", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
+
+ output = rails("db:migrate:up", "VERSION=unknown", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
+
+ output = rails("db:migrate:down", "VERSION=unknown", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
+ end
+
test "migration status after rollback and redo without timestamps" do
add_to_config("config.active_record.timestamped_migrations = false")
- Dir.chdir(app_path) do
- rails "generate", "model", "user", "username:string", "password:string"
- rails "generate", "migration", "add_email_to_users", "email:string"
- rails "db:migrate"
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+ rails "db:migrate"
- output = rails("db:migrate:status")
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{3,}\s+Create users/, output)
- assert_match(/up\s+\d{3,}\s+Add email to users/, output)
+ assert_match(/up\s+\d{3,}\s+Create users/, output)
+ assert_match(/up\s+\d{3,}\s+Add email to users/, output)
- rails "db:rollback", "STEP=2"
- output = rails("db:migrate:status")
+ rails "db:rollback", "STEP=2"
+ output = rails("db:migrate:status")
- assert_match(/down\s+\d{3,}\s+Create users/, output)
- assert_match(/down\s+\d{3,}\s+Add email to users/, output)
+ assert_match(/down\s+\d{3,}\s+Create users/, output)
+ assert_match(/down\s+\d{3,}\s+Add email to users/, output)
- rails "db:migrate:redo"
- output = rails("db:migrate:status")
+ rails "db:migrate:redo"
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{3,}\s+Create users/, output)
- assert_match(/up\s+\d{3,}\s+Add email to users/, output)
- end
+ assert_match(/up\s+\d{3,}\s+Create users/, output)
+ assert_match(/up\s+\d{3,}\s+Add email to users/, output)
end
test "running migrations with not timestamp head migration files" do
- Dir.chdir(app_path) do
-
- app_file "db/migrate/1_one_migration.rb", <<-MIGRATION
- class OneMigration < ActiveRecord::Migration::Current
- end
- MIGRATION
+ app_file "db/migrate/1_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
- app_file "db/migrate/02_two_migration.rb", <<-MIGRATION
- class TwoMigration < ActiveRecord::Migration::Current
- end
- MIGRATION
+ app_file "db/migrate/02_two_migration.rb", <<-MIGRATION
+ class TwoMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
- rails "db:migrate"
+ rails "db:migrate"
- output = rails("db:migrate:status")
+ output = rails("db:migrate:status")
- assert_match(/up\s+001\s+One migration/, output)
- assert_match(/up\s+002\s+Two migration/, output)
- end
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
end
test "schema generation when dump_schema_after_migration is set" do
diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb
new file mode 100644
index 0000000000..07d96fcb56
--- /dev/null
+++ b/railties/test/application/rake/multi_dbs_test.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+
+module ApplicationTests
+ module RakeTests
+ class RakeMultiDbsTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app(multi_db: true)
+ FileUtils.rm_rf("#{app_path}/config/environments")
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ def db_create_and_drop(namespace, expected_database, environment_loaded: true)
+ Dir.chdir(app_path) do
+ output = rails("db:create")
+ assert_match(/Created database/, output)
+ assert_match_namespace(namespace, output)
+ assert File.exist?(expected_database)
+
+ output = rails("db:drop")
+ assert_match(/Dropped database/, output)
+ assert_match_namespace(namespace, output)
+ assert_not File.exist?(expected_database)
+ end
+ end
+
+ def db_create_and_drop_namespace(namespace, expected_database, environment_loaded: true)
+ Dir.chdir(app_path) do
+ output = rails("db:create:#{namespace}")
+ assert_match(/Created database/, output)
+ assert_match_namespace(namespace, output)
+ assert File.exist?(expected_database)
+
+ output = rails("db:drop:#{namespace}")
+ assert_match(/Dropped database/, output)
+ assert_match_namespace(namespace, output)
+ assert_not File.exist?(expected_database)
+ end
+ end
+
+ def assert_match_namespace(namespace, output)
+ if namespace == "primary"
+ assert_match(/#{Rails.env}.sqlite3/, output)
+ else
+ assert_match(/#{Rails.env}_#{namespace}.sqlite3/, output)
+ end
+ end
+
+ def db_migrate_and_schema_dump_and_load(namespace, expected_database, format)
+ Dir.chdir(app_path) do
+ rails "generate", "model", "book", "title:string"
+ rails "generate", "model", "dog", "name:string"
+ write_models_for_animals
+ rails "db:migrate", "db:#{format}:dump"
+
+ if format == "schema"
+ schema_dump = File.read("db/#{format}.rb")
+ schema_dump_animals = File.read("db/animals_#{format}.rb")
+ assert_match(/create_table \"books\"/, schema_dump)
+ assert_match(/create_table \"dogs\"/, schema_dump_animals)
+ else
+ schema_dump = File.read("db/#{format}.sql")
+ schema_dump_animals = File.read("db/animals_#{format}.sql")
+ assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"books\"/, schema_dump)
+ assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"dogs\"/, schema_dump_animals)
+ end
+
+ rails "db:#{format}:load"
+
+ ar_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip }
+ animals_tables = lambda { rails("runner", "p AnimalsBase.connection.tables").strip }
+
+ assert_equal '["schema_migrations", "ar_internal_metadata", "books"]', ar_tables[]
+ assert_equal '["schema_migrations", "ar_internal_metadata", "dogs"]', animals_tables[]
+ end
+ end
+
+ def db_migrate_namespaced(namespace, expected_database)
+ Dir.chdir(app_path) do
+ rails "generate", "model", "book", "title:string"
+ rails "generate", "model", "dog", "name:string"
+ write_models_for_animals
+ output = rails("db:migrate:#{namespace}")
+ if namespace == "primary"
+ assert_match(/CreateBooks: migrated/, output)
+ else
+ assert_match(/CreateDogs: migrated/, output)
+ end
+ end
+ end
+
+ def write_models_for_animals
+ # make a directory for the animals migration
+ FileUtils.mkdir_p("#{app_path}/db/animals_migrate")
+ # move the dogs migration if it unless it already lives there
+ FileUtils.mv(Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first, "db/animals_migrate/") unless Dir.glob("#{app_path}/db/animals_migrate/**/*dogs.rb").first
+ # delete the dogs migration if it's still present in the
+ # migrate folder. This is necessary because sometimes
+ # the code isn't fast enough and an extra migration gets made
+ FileUtils.rm(Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first) if Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first
+
+ # change the base of the dog model
+ app_path("/app/models/dog.rb") do |file_name|
+ file = File.read("#{app_path}/app/models/dog.rb")
+ file.sub!(/ApplicationRecord/, "AnimalsBase")
+ File.write(file_name, file)
+ end
+
+ # create the base model for dog to inherit from
+ File.open("#{app_path}/app/models/animals_base.rb", "w") do |file|
+ file.write(<<-EOS
+class AnimalsBase < ActiveRecord::Base
+ self.abstract_class = true
+
+ establish_connection :animals
+end
+EOS
+)
+ end
+ end
+
+ test "db:create and db:drop works on all databases for env" do
+ require "#{app_path}/config/environment"
+ ActiveRecord::Base.configurations[Rails.env].each do |namespace, config|
+ db_create_and_drop namespace, config["database"]
+ end
+ end
+
+ test "db:create:namespace and db:drop:namespace works on specified databases" do
+ require "#{app_path}/config/environment"
+ ActiveRecord::Base.configurations[Rails.env].each do |namespace, config|
+ db_create_and_drop_namespace namespace, config["database"]
+ end
+ end
+
+ test "db:migrate and db:schema:dump and db:schema:load works on all databases" do
+ require "#{app_path}/config/environment"
+ ActiveRecord::Base.configurations[Rails.env].each do |namespace, config|
+ db_migrate_and_schema_dump_and_load namespace, config["database"], "schema"
+ end
+ end
+
+ test "db:migrate and db:structure:dump and db:structure:load works on all databases" do
+ require "#{app_path}/config/environment"
+ ActiveRecord::Base.configurations[Rails.env].each do |namespace, config|
+ db_migrate_and_schema_dump_and_load namespace, config["database"], "structure"
+ end
+ end
+
+ test "db:migrate:namespace works" do
+ require "#{app_path}/config/environment"
+ ActiveRecord::Base.configurations[Rails.env].each do |namespace, config|
+ db_migrate_namespaced namespace, config["database"]
+ end
+ end
+ end
+ end
+end
diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb
index 8e9fe9b6b4..d73e5cdfa3 100644
--- a/railties/test/application/rake/notes_test.rb
+++ b/railties/test/application/rake/notes_test.rb
@@ -101,7 +101,7 @@ module ApplicationTests
task :notes_custom do
tags = 'TODO|FIXME'
opts = { dirs: %w(lib test), tag: true }
- SourceAnnotationExtractor.enumerate(tags, opts)
+ Rails::SourceAnnotationExtractor.enumerate(tags, opts)
end
EOS
diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb
index 76bc0ce1d7..1522a2bbc5 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -2,7 +2,6 @@
require "isolation/abstract_unit"
require "env_helpers"
-require "active_support/core_ext/string/strip"
module ApplicationTests
class RakeTest < ActiveSupport::TestCase
@@ -40,9 +39,9 @@ module ApplicationTests
with_rails_env "test" do
rails "generate", "model", "product", "name:string"
rails "db:create", "db:migrate"
- output = Dir.chdir(app_path) { rails("db:test:prepare", "test") }
+ output = rails("db:test:prepare", "test")
- refute_match(/ActiveRecord::ProtectedEnvironmentError/, output)
+ assert_no_match(/ActiveRecord::ProtectedEnvironmentError/, output)
end
end
@@ -101,6 +100,7 @@ module ApplicationTests
add_to_config <<-RUBY
rake_tasks do
task do_nothing: :environment do
+ puts 'There is nothing'
end
end
RUBY
@@ -113,10 +113,8 @@ module ApplicationTests
raise 'should not be pre-required for rake even eager_load=true'
RUBY
- Dir.chdir(app_path) do
- assert system("bin/rails do_nothing RAILS_ENV=production"),
- "should not be pre-required for rake even eager_load=true"
- end
+ output = rails("do_nothing", "RAILS_ENV=production")
+ assert_match "There is nothing", output
end
def test_code_statistics_sanity
@@ -124,138 +122,6 @@ module ApplicationTests
rails("stats")
end
- def test_rails_routes_calls_the_route_inspector
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- end
- RUBY
-
- output = rails("routes")
- assert_equal <<-MESSAGE.strip_heredoc, output
- Prefix Verb URI Pattern Controller#Action
- cart GET /cart(.:format) cart#show
- MESSAGE
- end
-
- def test_singular_resource_output_in_rake_routes
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- resource :post
- end
- RUBY
-
- expected_output = [" Prefix Verb URI Pattern Controller#Action",
- " new_post GET /post/new(.:format) posts#new",
- "edit_post GET /post/edit(.:format) posts#edit",
- " post GET /post(.:format) posts#show",
- " PATCH /post(.:format) posts#update",
- " PUT /post(.:format) posts#update",
- " DELETE /post(.:format) posts#destroy",
- " POST /post(.:format) posts#create\n"].join("\n")
-
- output = rails("routes", "-c", "PostController")
- assert_equal expected_output, output
- end
-
- def test_rails_routes_with_global_search_key
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- post '/cart', to: 'cart#create'
- get '/basketballs', to: 'basketball#index'
- end
- RUBY
-
- output = rails("routes", "-g", "show", allow_failure: true)
- assert_equal <<-MESSAGE.strip_heredoc, output
- Prefix Verb URI Pattern Controller#Action
- cart GET /cart(.:format) cart#show
- MESSAGE
-
- output = rails("routes", "-g", "POST")
- assert_equal <<-MESSAGE.strip_heredoc, output
- Prefix Verb URI Pattern Controller#Action
- POST /cart(.:format) cart#create
- MESSAGE
-
- output = rails("routes", "-g", "basketballs")
- assert_equal " Prefix Verb URI Pattern Controller#Action\n" \
- "basketballs GET /basketballs(.:format) basketball#index\n", output
- end
-
- def test_rails_routes_with_controller_search_key
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- get '/basketball', to: 'basketball#index'
- end
- RUBY
-
- output = rails("routes", "-c", "cart")
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
-
- output = rails("routes", "-c", "Cart")
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
-
- output = rails("routes", "-c", "CartController")
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
- end
-
- def test_rails_routes_with_namespaced_controller_search_key
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- namespace :admin do
- resource :post
- end
- end
- RUBY
- expected_output = [" Prefix Verb URI Pattern Controller#Action",
- " new_admin_post GET /admin/post/new(.:format) admin/posts#new",
- "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit",
- " admin_post GET /admin/post(.:format) admin/posts#show",
- " PATCH /admin/post(.:format) admin/posts#update",
- " PUT /admin/post(.:format) admin/posts#update",
- " DELETE /admin/post(.:format) admin/posts#destroy",
- " POST /admin/post(.:format) admin/posts#create\n"].join("\n")
-
- output = rails("routes", "-c", "Admin::PostController")
- assert_equal expected_output, output
-
- output = rails("routes", "-c", "PostController")
- assert_equal expected_output, output
- end
-
- def test_rails_routes_displays_message_when_no_routes_are_defined
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- end
- RUBY
-
- assert_equal <<-MESSAGE.strip_heredoc, rails("routes")
- You don't have any routes defined!
-
- Please add some routes in config/routes.rb.
-
- For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html.
- MESSAGE
- end
-
- def test_rake_routes_with_rake_options
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- end
- RUBY
-
- output = Dir.chdir(app_path) { `bin/rake --rakefile Rakefile routes` }
-
- assert_equal <<-MESSAGE.strip_heredoc, output
- Prefix Verb URI Pattern Controller#Action
- cart GET /cart(.:format) cart#show
- MESSAGE
- end
-
def test_logger_is_flushed_when_exiting_production_rake_tasks
add_to_config <<-RUBY
rake_tasks do
@@ -294,9 +160,8 @@ module ApplicationTests
def test_scaffold_tests_pass_by_default
rails "generate", "scaffold", "user", "username:string", "password:string"
- output = Dir.chdir(app_path) do
- `RAILS_ENV=test bin/rails db:migrate test`
- end
+ with_rails_env("test") { rails("db:migrate") }
+ output = rails("test")
assert_match(/7 runs, 9 assertions, 0 failures, 0 errors/, output)
assert_no_match(/Errors running/, output)
@@ -313,9 +178,8 @@ module ApplicationTests
RUBY
rails "generate", "scaffold", "user", "username:string", "password:string"
- output = Dir.chdir(app_path) do
- `RAILS_ENV=test bin/rails db:migrate test`
- end
+ with_rails_env("test") { rails("db:migrate") }
+ output = rails("test")
assert_match(/5 runs, 7 assertions, 0 failures, 0 errors/, output)
assert_no_match(/Errors running/, output)
@@ -325,9 +189,8 @@ module ApplicationTests
rails "generate", "model", "Product"
rails "generate", "model", "Cart"
rails "generate", "scaffold", "LineItems", "product:references", "cart:belongs_to"
- output = Dir.chdir(app_path) do
- `RAILS_ENV=test bin/rails db:migrate test`
- end
+ with_rails_env("test") { rails("db:migrate") }
+ output = rails("test")
assert_match(/7 runs, 9 assertions, 0 failures, 0 errors/, output)
assert_no_match(/Errors running/, output)
@@ -337,9 +200,7 @@ module ApplicationTests
add_to_config "config.active_record.schema_format = :sql"
rails "generate", "scaffold", "user", "username:string"
rails "db:migrate"
- output = with_rails_env("test") do
- rails "db:test:prepare", "--trace"
- end
+ output = rails("db:test:prepare", "--trace")
assert_match(/Execute db:test:load_structure/, output)
end
@@ -368,18 +229,16 @@ module ApplicationTests
def test_rake_clear_schema_cache
rails "db:schema:cache:dump", "db:schema:cache:clear"
- assert !File.exist?(File.join(app_path, "db", "schema_cache.yml"))
+ assert_not File.exist?(File.join(app_path, "db", "schema_cache.yml"))
end
def test_copy_templates
- Dir.chdir(app_path) do
- rails "app:templates:copy"
- %w(controller mailer scaffold).each do |dir|
- assert File.exist?(File.join(app_path, "lib", "templates", "erb", dir))
- end
- %w(controller helper scaffold_controller assets).each do |dir|
- assert File.exist?(File.join(app_path, "lib", "templates", "rails", dir))
- end
+ rails "app:templates:copy"
+ %w(controller mailer scaffold).each do |dir|
+ assert File.exist?(File.join(app_path, "lib", "templates", "erb", dir))
+ end
+ %w(controller helper scaffold_controller assets).each do |dir|
+ assert File.exist?(File.join(app_path, "lib", "templates", "rails", dir))
end
end
diff --git a/railties/test/application/rendering_test.rb b/railties/test/application/rendering_test.rb
index 3724886c54..ab1591f388 100644
--- a/railties/test/application/rendering_test.rb
+++ b/railties/test/application/rendering_test.rb
@@ -4,7 +4,7 @@ require "isolation/abstract_unit"
require "rack/test"
module ApplicationTests
- class RoutingTest < ActiveSupport::TestCase
+ class RenderingTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
include Rack::Test::Methods
diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb
index 64c46c4b45..8f5f48c281 100644
--- a/railties/test/application/runner_test.rb
+++ b/railties/test/application/runner_test.rb
@@ -107,13 +107,13 @@ module ApplicationTests
def test_runner_detects_syntax_errors
output = rails("runner", "puts 'hello world", allow_failure: true)
- assert_not $?.success?
+ assert_not_predicate $?, :success?
assert_match "unterminated string meets end of file", output
end
def test_runner_detects_bad_script_name
output = rails("runner", "iuiqwiourowe", allow_failure: true)
- assert_not $?.success?
+ assert_not_predicate $?, :success?
assert_match "undefined local variable or method `iuiqwiourowe' for", output
end
@@ -128,5 +128,17 @@ module ApplicationTests
assert_match "production", rails("runner", "puts Rails.env")
end
end
+
+ def test_can_call_same_name_class_as_defined_in_thor
+ app_file "app/models/task.rb", <<-MODEL
+ class Task
+ def self.count
+ 42
+ end
+ end
+ MODEL
+
+ assert_match "42", rails("runner", "puts Task.count")
+ end
end
end
diff --git a/railties/test/application/server_test.rb b/railties/test/application/server_test.rb
index 6db9a3b9e8..f3a7e00a4d 100644
--- a/railties/test/application/server_test.rb
+++ b/railties/test/application/server_test.rb
@@ -29,12 +29,17 @@ module ApplicationTests
server.app
log = File.read(Rails.application.config.paths["log"].first)
- assert_match(/DEPRECATION WARNING: Use `Rails::Application` subclass to start the server is deprecated/, log)
+ assert_match(/DEPRECATION WARNING: Using `Rails::Application` subclass to start the server is deprecated/, log)
end
test "restart rails server with custom pid file path" do
skip "PTY unavailable" unless available_pty?
+ File.open("#{app_path}/config/boot.rb", "w") do |f|
+ f.puts "ENV['BUNDLE_GEMFILE'] = '#{Bundler.default_gemfile.to_s}'"
+ f.puts "require 'bundler/setup'"
+ end
+
master, slave = PTY.open
pid = nil
@@ -42,7 +47,7 @@ module ApplicationTests
pid = Process.spawn("#{app_path}/bin/rails server -P tmp/dummy.pid", in: slave, out: slave, err: slave)
assert_output("Listening", master)
- Dir.chdir(app_path) { system("bin/rails restart") }
+ rails("restart")
assert_output("Restarting", master)
assert_output("Inherited", master)
diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb
index e92a0466dd..8e5ccf94cc 100644
--- a/railties/test/application/test_runner_test.rb
+++ b/railties/test/application/test_runner_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "isolation/abstract_unit"
-require "active_support/core_ext/string/strip"
require "env_helpers"
module ApplicationTests
@@ -502,10 +501,10 @@ module ApplicationTests
end
def test_output_inline_by_default
- create_test_file :models, "post", pass: false
+ create_test_file :models, "post", pass: false, print: false
output = run_test_command("test/models/post_test.rb")
- expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/models/post_test.rb:6\]:\nwups!\n\nbin/rails test test/models/post_test.rb:4\n\n\n\n}
+ expect = %r{Running:\n\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/models/post_test.rb:6\]:\nwups!\n\nbin/rails test test/models/post_test.rb:4\n\n\n\n}
assert_match expect, output
end
@@ -523,6 +522,28 @@ module ApplicationTests
capture(:stderr) { run_test_command("test/models/post_test.rb --fail-fast", stderr: true) })
end
+ def test_run_in_parallel_with_processes
+ file_name = create_parallel_processes_test_file
+
+ output = run_test_command(file_name)
+
+ assert_match %r{Finished in.*\n2 runs, 2 assertions}, output
+ end
+
+ def test_run_in_parallel_with_threads
+ app_path("/test/test_helper.rb") do |file_name|
+ file = File.read(file_name)
+ file.sub!(/parallelize\(([^\)]*)\)/, "parallelize(\\1, with: :threads)")
+ File.write(file_name, file)
+ end
+
+ file_name = create_parallel_threads_test_file
+
+ output = run_test_command(file_name)
+
+ assert_match %r{Finished in.*\n2 runs, 2 assertions}, output
+ end
+
def test_raise_error_when_specified_file_does_not_exist
error = capture(:stderr) { run_test_command("test/not_exists.rb", stderr: true) }
assert_match(%r{cannot load such file.+test/not_exists\.rb}, error)
@@ -569,6 +590,40 @@ module ApplicationTests
assert_match "AccountTest#test_truth", output, "passing TEST= should run selected test"
end
+ def test_running_with_ruby_gets_test_env_by_default
+ # Subshells inherit `ENV`, so we need to ensure `RAILS_ENV` is set to
+ # nil before we run the tests in the test app.
+ re, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], nil
+
+ file = create_test_for_env("test")
+ results = Dir.chdir(app_path) {
+ `ruby -Ilib:test #{file}`.each_line.map { |line| JSON.parse line }
+ }
+ assert_equal 1, results.length
+ failures = results.first["failures"]
+ flunk(failures.first) unless failures.empty?
+
+ ensure
+ ENV["RAILS_ENV"] = re
+ end
+
+ def test_running_with_ruby_can_set_env_via_cmdline
+ # Subshells inherit `ENV`, so we need to ensure `RAILS_ENV` is set to
+ # nil before we run the tests in the test app.
+ re, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], nil
+
+ file = create_test_for_env("development")
+ results = Dir.chdir(app_path) {
+ `RAILS_ENV=development ruby -Ilib:test #{file}`.each_line.map { |line| JSON.parse line }
+ }
+ assert_equal 1, results.length
+ failures = results.first["failures"]
+ flunk(failures.first) unless failures.empty?
+
+ ensure
+ ENV["RAILS_ENV"] = re
+ 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'` }
@@ -684,7 +739,7 @@ module ApplicationTests
def create_model_with_fixture
rails "generate", "model", "user", "name:string"
- app_file "test/fixtures/users.yml", <<-YAML.strip_heredoc
+ app_file "test/fixtures/users.yml", <<~YAML
vampire:
id: 1
name: Koyomi Araragi
@@ -727,19 +782,109 @@ module ApplicationTests
app_file "db/schema.rb", ""
end
- def create_test_file(path = :unit, name = "test", pass: true)
+ def create_test_for_env(env)
+ app_file "test/models/environment_test.rb", <<-RUBY
+ require 'test_helper'
+ class JSONReporter < Minitest::AbstractReporter
+ def record(result)
+ puts JSON.dump(klass: result.class.name,
+ name: result.name,
+ failures: result.failures,
+ assertions: result.assertions,
+ time: result.time)
+ end
+ end
+
+ def Minitest.plugin_json_reporter_init(opts)
+ Minitest.reporter.reporters.clear
+ Minitest.reporter.reporters << JSONReporter.new
+ end
+
+ Minitest.extensions << "rails"
+ Minitest.extensions << "json_reporter"
+
+ # Minitest uses RubyGems to find plugins, and since RubyGems
+ # doesn't know about the Rails installation we're pointing at,
+ # Minitest won't require the Rails minitest plugin when we run
+ # these integration tests. So we have to manually require the
+ # Minitest plugin here.
+ require 'minitest/rails_plugin'
+
+ class EnvironmentTest < ActiveSupport::TestCase
+ def test_environment
+ test_db = ActiveRecord::Base.configurations[#{env.dump}]["database"]
+ db_file = ActiveRecord::Base.connection_config[:database]
+ assert_match(test_db, db_file)
+ assert_equal #{env.dump}, ENV["RAILS_ENV"]
+ end
+ end
+ RUBY
+ end
+
+ def create_test_file(path = :unit, name = "test", pass: true, print: true)
app_file "test/#{path}/#{name}_test.rb", <<-RUBY
require 'test_helper'
class #{name.camelize}Test < ActiveSupport::TestCase
def test_truth
- puts "#{name.camelize}Test"
+ puts "#{name.camelize}Test" if #{print}
assert #{pass}, 'wups!'
end
end
RUBY
end
+ def create_parallel_processes_test_file
+ app_file "test/models/parallel_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class ParallelTest < ActiveSupport::TestCase
+ RD1, WR1 = IO.pipe
+ RD2, WR2 = IO.pipe
+
+ test "one" do
+ WR1.close
+ assert_equal "x", RD1.read(1) # blocks until two runs
+
+ RD2.close
+ WR2.write "y" # Allow two to run
+ WR2.close
+ end
+
+ test "two" do
+ RD1.close
+ WR1.write "x" # Allow one to run
+ WR1.close
+
+ WR2.close
+ assert_equal "y", RD2.read(1) # blocks until one runs
+ end
+ end
+ RUBY
+ end
+
+ def create_parallel_threads_test_file
+ app_file "test/models/parallel_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class ParallelTest < ActiveSupport::TestCase
+ Q1 = Queue.new
+ Q2 = Queue.new
+ test "one" do
+ assert_equal "x", Q1.pop # blocks until two runs
+
+ Q2 << "y"
+ end
+
+ test "two" do
+ Q1 << "x"
+
+ assert_equal "y", Q2.pop # blocks until one runs
+ end
+ end
+ RUBY
+ end
+
def create_env_test
app_file "test/unit/env_test.rb", <<-RUBY
require 'test_helper'
diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb
index 0a51e98656..fb43bebfbe 100644
--- a/railties/test/application/test_test.rb
+++ b/railties/test/application/test_test.rb
@@ -7,10 +7,15 @@ module ApplicationTests
include ActiveSupport::Testing::Isolation
def setup
+ @old = ENV["PARALLEL_WORKERS"]
+ ENV["PARALLEL_WORKERS"] = "0"
+
build_app
end
def teardown
+ ENV["PARALLEL_WORKERS"] = @old
+
teardown_app
end
diff --git a/railties/test/command/spellchecker_test.rb b/railties/test/command/spellchecker_test.rb
new file mode 100644
index 0000000000..e6a7a3957c
--- /dev/null
+++ b/railties/test/command/spellchecker_test.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "rails/command/spellchecker"
+
+class Rails::Command::SpellcheckerTest < ActiveSupport::TestCase
+ test "suggests a word correction from dictionary" do
+ assert_equal "thin", Rails::Command::Spellchecker.suggest("tin", from: %w(puma thin cgi))
+ end
+end
diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb
index 7eb26c355c..b7cdb8229e 100644
--- a/railties/test/commands/console_test.rb
+++ b/railties/test/commands/console_test.rb
@@ -20,30 +20,30 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
def test_sandbox_option
console = Rails::Console.new(app, parse_arguments(["--sandbox"]))
- assert console.sandbox?
+ assert_predicate console, :sandbox?
end
def test_short_version_of_sandbox_option
console = Rails::Console.new(app, parse_arguments(["-s"]))
- assert console.sandbox?
+ assert_predicate console, :sandbox?
end
def test_no_options
console = Rails::Console.new(app, parse_arguments([]))
- assert !console.sandbox?
+ assert_not_predicate console, :sandbox?
end
def test_start
start
- assert app.console.started?
+ assert_predicate app.console, :started?
assert_match(/Loading \w+ environment \(Rails/, output)
end
def test_start_with_sandbox
start ["--sandbox"]
- assert app.console.started?
+ assert_predicate app.console, :started?
assert app.sandbox
assert_match(/Loading \w+ environment in sandbox \(Rails/, output)
end
@@ -172,21 +172,8 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
end
def parse_arguments(args)
- Rails::Command::ConsoleCommand.class_eval do
- alias_method :old_perform, :perform
- define_method(:perform) do
- extract_environment_option_from_argument
-
- options
- end
- end
-
- Rails::Command.invoke(:console, args)
- ensure
- Rails::Command::ConsoleCommand.class_eval do
- undef_method :perform
- alias_method :perform, :old_perform
- undef_method :old_perform
- end
+ command = Rails::Command::ConsoleCommand.new([], args)
+ command.send(:extract_environment_option_from_argument)
+ command.options
end
end
diff --git a/railties/test/commands/credentials_test.rb b/railties/test/commands/credentials_test.rb
index 743fb5f788..663ee73bcd 100644
--- a/railties/test/commands/credentials_test.rb
+++ b/railties/test/commands/credentials_test.rb
@@ -13,7 +13,10 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
teardown { teardown_app }
test "edit without editor gives hint" do
- assert_match "No $EDITOR to open credentials in", run_edit_command(editor: "")
+ run_edit_command(editor: "").tap do |output|
+ assert_match "No $EDITOR to open file in", output
+ assert_match "bin/rails credentials:edit", output
+ end
end
test "edit credentials" do
@@ -23,10 +26,6 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
end
end
- test "show credentials" do
- assert_match(/access_key_id: 123/, run_show_command)
- end
-
test "edit command does not add master key to gitignore when already exist" do
run_edit_command
@@ -36,6 +35,44 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
end
end
+ test "edit command does not overwrite by default if credentials already exists" do
+ run_edit_command(editor: "eval echo api_key: abc >")
+ assert_match(/api_key: abc/, run_show_command)
+
+ run_edit_command
+ assert_match(/api_key: abc/, run_show_command)
+ end
+
+ test "edit command does not add master key when `RAILS_MASTER_KEY` env specified" do
+ Dir.chdir(app_path) do
+ key = IO.binread("config/master.key").strip
+ FileUtils.rm("config/master.key")
+
+ switch_env("RAILS_MASTER_KEY", key) do
+ run_edit_command
+ assert_not File.exist?("config/master.key")
+ end
+ end
+ end
+
+ test "show credentials" do
+ assert_match(/access_key_id: 123/, run_show_command)
+ end
+
+ test "show command raise error when require_master_key is specified and key does not exist" do
+ remove_file "config/master.key"
+ add_to_config "config.require_master_key = true"
+
+ assert_match(/Missing encryption key to decrypt file with/, run_show_command(allow_failure: true))
+ end
+
+ test "show command does not raise error when require_master_key is false and master key does not exist" do
+ remove_file "config/master.key"
+ add_to_config "config.require_master_key = false"
+
+ assert_match(/Missing master key to decrypt credentials/, run_show_command)
+ end
+
private
def run_edit_command(editor: "cat")
switch_env("EDITOR", editor) do
@@ -43,7 +80,7 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
end
end
- def run_show_command
- rails "credentials:show"
+ def run_show_command(**options)
+ rails "credentials:show", **options
end
end
diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb
index 6ad96b28c7..0aea21051a 100644
--- a/railties/test/commands/dbconsole_test.rb
+++ b/railties/test/commands/dbconsole_test.rb
@@ -123,31 +123,31 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
def test_mysql
start(adapter: "mysql2", database: "db")
- assert !aborted
+ assert_not aborted
assert_equal [%w[mysql mysql5], "db"], dbconsole.find_cmd_and_exec_args
end
def test_mysql_full
start(adapter: "mysql2", database: "db", host: "locahost", port: 1234, socket: "socket", username: "user", password: "qwerty", encoding: "UTF-8")
- assert !aborted
+ assert_not aborted
assert_equal [%w[mysql mysql5], "--host=locahost", "--port=1234", "--socket=socket", "--user=user", "--default-character-set=UTF-8", "-p", "db"], dbconsole.find_cmd_and_exec_args
end
def test_mysql_include_password
start({ adapter: "mysql2", database: "db", username: "user", password: "qwerty" }, ["-p"])
- assert !aborted
+ assert_not aborted
assert_equal [%w[mysql mysql5], "--user=user", "--password=qwerty", "db"], dbconsole.find_cmd_and_exec_args
end
def test_postgresql
start(adapter: "postgresql", database: "db")
- assert !aborted
+ assert_not aborted
assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args
end
def test_postgresql_full
start(adapter: "postgresql", database: "db", username: "user", password: "q1w2e3", host: "host", port: 5432)
- assert !aborted
+ assert_not aborted
assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args
assert_equal "user", ENV["PGUSER"]
assert_equal "host", ENV["PGHOST"]
@@ -157,7 +157,7 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
def test_postgresql_include_password
start({ adapter: "postgresql", database: "db", username: "user", password: "q1w2e3" }, ["-p"])
- assert !aborted
+ assert_not aborted
assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args
assert_equal "user", ENV["PGUSER"]
assert_equal "q1w2e3", ENV["PGPASSWORD"]
@@ -165,13 +165,13 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
def test_sqlite3
start(adapter: "sqlite3", database: "db.sqlite3")
- assert !aborted
+ assert_not aborted
assert_equal ["sqlite3", Rails.root.join("db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_mode
start({ adapter: "sqlite3", database: "db.sqlite3" }, ["--mode", "html"])
- assert !aborted
+ assert_not aborted
assert_equal ["sqlite3", "-html", Rails.root.join("db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args
end
@@ -182,27 +182,27 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
def test_sqlite3_db_absolute_path
start(adapter: "sqlite3", database: "/tmp/db.sqlite3")
- assert !aborted
+ assert_not aborted
assert_equal ["sqlite3", "/tmp/db.sqlite3"], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_db_without_defined_rails_root
Rails.stub(:respond_to?, false) do
start(adapter: "sqlite3", database: "config/db.sqlite3")
- assert !aborted
+ assert_not aborted
assert_equal ["sqlite3", Rails.root.join("../config/db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args
end
end
def test_oracle
start(adapter: "oracle", database: "db", username: "user", password: "secret")
- assert !aborted
+ assert_not aborted
assert_equal ["sqlplus", "user@db"], dbconsole.find_cmd_and_exec_args
end
def test_oracle_include_password
start({ adapter: "oracle", database: "db", username: "user", password: "secret" }, ["-p"])
- assert !aborted
+ assert_not aborted
assert_equal ["sqlplus", "user/secret@db"], dbconsole.find_cmd_and_exec_args
end
diff --git a/railties/test/commands/encrypted_test.rb b/railties/test/commands/encrypted_test.rb
new file mode 100644
index 0000000000..9fc73d5f18
--- /dev/null
+++ b/railties/test/commands/encrypted_test.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "env_helpers"
+require "rails/command"
+require "rails/commands/encrypted/encrypted_command"
+
+class Rails::Command::EncryptedCommandTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation, EnvHelpers
+
+ setup :build_app
+ teardown :teardown_app
+
+ test "edit without editor gives hint" do
+ run_edit_command("config/tokens.yml.enc", editor: "").tap do |output|
+ assert_match "No $EDITOR to open file in", output
+ assert_match "bin/rails encrypted:edit", output
+ end
+ end
+
+ test "edit encrypted file" do
+ # Run twice to ensure file can be reread after first edit pass.
+ 2.times do
+ assert_match(/access_key_id: 123/, run_edit_command("config/tokens.yml.enc"))
+ end
+ end
+
+ test "edit command does not add master key to gitignore when already exist" do
+ run_edit_command("config/tokens.yml.enc")
+
+ Dir.chdir(app_path) do
+ assert_match "/config/master.key", File.read(".gitignore")
+ end
+ end
+
+ test "edit command does not add master key when `RAILS_MASTER_KEY` env specified" do
+ Dir.chdir(app_path) do
+ key = IO.binread("config/master.key").strip
+ FileUtils.rm("config/master.key")
+
+ switch_env("RAILS_MASTER_KEY", key) do
+ run_edit_command("config/tokens.yml.enc")
+ assert_not File.exist?("config/master.key")
+ end
+ end
+ end
+
+ test "edit encrypts file with custom key" do
+ run_edit_command("config/tokens.yml.enc", key: "config/tokens.key")
+
+ Dir.chdir(app_path) do
+ assert File.exist?("config/tokens.yml.enc")
+ assert File.exist?("config/tokens.key")
+
+ assert_match "/config/tokens.key", File.read(".gitignore")
+ end
+
+ assert_match(/access_key_id: 123/, run_edit_command("config/tokens.yml.enc", key: "config/tokens.key"))
+ end
+
+ test "show encrypted file with custom key" do
+ run_edit_command("config/tokens.yml.enc", key: "config/tokens.key")
+
+ assert_match(/access_key_id: 123/, run_show_command("config/tokens.yml.enc", key: "config/tokens.key"))
+ end
+
+ test "show command raise error when require_master_key is specified and key does not exist" do
+ add_to_config "config.require_master_key = true"
+
+ assert_match(/Missing encryption key to decrypt file with/,
+ run_show_command("config/tokens.yml.enc", key: "unexist.key", allow_failure: true))
+ end
+
+ test "show command does not raise error when require_master_key is false and master key does not exist" do
+ remove_file "config/master.key"
+ add_to_config "config.require_master_key = false"
+
+ assert_match(/Missing 'config\/master\.key' to decrypt data/, run_show_command("config/tokens.yml.enc"))
+ end
+
+ test "won't corrupt encrypted file when passed wrong key" do
+ run_edit_command("config/tokens.yml.enc", key: "config/tokens.key")
+
+ assert_match "passed the wrong key",
+ run_edit_command("config/tokens.yml.enc", allow_failure: true)
+
+ assert_match(/access_key_id: 123/, run_show_command("config/tokens.yml.enc", key: "config/tokens.key"))
+ end
+
+ private
+ def run_edit_command(file, key: nil, editor: "cat", **options)
+ switch_env("EDITOR", editor) do
+ rails "encrypted:edit", prepare_args(file, key), **options
+ end
+ end
+
+ def run_show_command(file, key: nil, **options)
+ rails "encrypted:show", prepare_args(file, key), **options
+ end
+
+ def prepare_args(file, key)
+ args = [ file ]
+ args.push("--key", key) if key
+ args
+ end
+end
diff --git a/railties/test/commands/routes_test.rb b/railties/test/commands/routes_test.rb
new file mode 100644
index 0000000000..77ed2bda61
--- /dev/null
+++ b/railties/test/commands/routes_test.rb
@@ -0,0 +1,175 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rails/command"
+require "rails/commands/routes/routes_command"
+require "io/console/size"
+
+class Rails::Command::RoutesTest < ActiveSupport::TestCase
+ setup :build_app
+ teardown :teardown_app
+
+ test "singular resource output in rails routes" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ resource :post
+ end
+ RUBY
+
+ expected_output = [" Prefix Verb URI Pattern Controller#Action",
+ " new_post GET /post/new(.:format) posts#new",
+ "edit_post GET /post/edit(.:format) posts#edit",
+ " post GET /post(.:format) posts#show",
+ " PATCH /post(.:format) posts#update",
+ " PUT /post(.:format) posts#update",
+ " DELETE /post(.:format) posts#destroy",
+ " POST /post(.:format) posts#create\n"].join("\n")
+
+ output = run_routes_command(["-c", "PostController"])
+ assert_equal expected_output, output
+ end
+
+ test "rails routes with global search key" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/cart', to: 'cart#show'
+ post '/cart', to: 'cart#create'
+ get '/basketballs', to: 'basketball#index'
+ end
+ RUBY
+
+ output = run_routes_command(["-g", "show"])
+ assert_equal <<~MESSAGE, output
+ Prefix Verb URI Pattern Controller#Action
+ cart GET /cart(.:format) cart#show
+ rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
+ rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
+ rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
+ MESSAGE
+
+ output = run_routes_command(["-g", "POST"])
+ assert_equal <<~MESSAGE, output
+ Prefix Verb URI Pattern Controller#Action
+ POST /cart(.:format) cart#create
+ rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
+ MESSAGE
+
+ output = run_routes_command(["-g", "basketballs"])
+ assert_equal " Prefix Verb URI Pattern Controller#Action\n" \
+ "basketballs GET /basketballs(.:format) basketball#index\n", output
+ end
+
+ test "rails routes with controller search key" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/cart', to: 'cart#show'
+ get '/basketball', to: 'basketball#index'
+ end
+ RUBY
+
+ output = run_routes_command(["-c", "cart"])
+ assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
+
+ output = run_routes_command(["-c", "Cart"])
+ assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
+
+ output = run_routes_command(["-c", "CartController"])
+ assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
+ end
+
+ test "rails routes with namespaced controller search key" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ namespace :admin do
+ resource :post
+ end
+ end
+ RUBY
+
+ expected_output = [" Prefix Verb URI Pattern Controller#Action",
+ " new_admin_post GET /admin/post/new(.:format) admin/posts#new",
+ "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit",
+ " admin_post GET /admin/post(.:format) admin/posts#show",
+ " PATCH /admin/post(.:format) admin/posts#update",
+ " PUT /admin/post(.:format) admin/posts#update",
+ " DELETE /admin/post(.:format) admin/posts#destroy",
+ " POST /admin/post(.:format) admin/posts#create\n"].join("\n")
+
+ output = run_routes_command(["-c", "Admin::PostController"])
+ assert_equal expected_output, output
+
+ output = run_routes_command(["-c", "PostController"])
+ assert_equal expected_output, output
+ end
+
+ test "rails routes displays message when no routes are defined" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ end
+ RUBY
+
+ assert_equal <<~MESSAGE, run_routes_command
+ Prefix Verb URI Pattern Controller#Action
+ rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
+ rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
+ rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
+ update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
+ rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
+ MESSAGE
+ end
+
+ test "rails routes with expanded option" do
+ begin
+ previous_console_winsize = IO.console.winsize
+ IO.console.winsize = [0, 27]
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/cart', to: 'cart#show'
+ end
+ RUBY
+
+ output = run_routes_command(["--expanded"])
+
+ assert_equal <<~MESSAGE, output
+ --[ Route 1 ]--------------
+ Prefix | cart
+ Verb | GET
+ URI | /cart(.:format)
+ Controller#Action | cart#show
+ --[ Route 2 ]--------------
+ Prefix | rails_service_blob
+ Verb | GET
+ URI | /rails/active_storage/blobs/:signed_id/*filename(.:format)
+ Controller#Action | active_storage/blobs#show
+ --[ Route 3 ]--------------
+ Prefix | rails_blob_representation
+ Verb | GET
+ URI | /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format)
+ Controller#Action | active_storage/representations#show
+ --[ Route 4 ]--------------
+ Prefix | rails_disk_service
+ Verb | GET
+ URI | /rails/active_storage/disk/:encoded_key/*filename(.:format)
+ Controller#Action | active_storage/disk#show
+ --[ Route 5 ]--------------
+ Prefix | update_rails_disk_service
+ Verb | PUT
+ URI | /rails/active_storage/disk/:encoded_token(.:format)
+ Controller#Action | active_storage/disk#update
+ --[ Route 6 ]--------------
+ Prefix | rails_direct_uploads
+ Verb | POST
+ URI | /rails/active_storage/direct_uploads(.:format)
+ Controller#Action | active_storage/direct_uploads#create
+ MESSAGE
+ ensure
+ IO.console.winsize = previous_console_winsize
+ end
+ end
+
+ private
+ def run_routes_command(args = [])
+ rails "routes", args
+ end
+end
diff --git a/railties/test/commands/secrets_test.rb b/railties/test/commands/secrets_test.rb
index 66a81826e3..6b9f284a0c 100644
--- a/railties/test/commands/secrets_test.rb
+++ b/railties/test/commands/secrets_test.rb
@@ -8,21 +8,38 @@ require "rails/commands/secrets/secrets_command"
class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation, EnvHelpers
- def setup
- build_app
+ setup :build_app
+ teardown :teardown_app
+
+ test "edit without editor gives hint" do
+ assert_match "No $EDITOR to open decrypted secrets in", run_edit_command(editor: "")
end
- def teardown
- teardown_app
+ test "encrypted secrets are deprecated when using credentials" do
+ assert_match "Encrypted secrets is deprecated", run_setup_command
+ assert_equal 1, $?.exitstatus
+ assert_not File.exist?("config/secrets.yml.enc")
end
- test "edit without editor gives hint" do
- assert_match "No $EDITOR to open decrypted secrets in", run_edit_command(editor: "")
+ test "encrypted secrets are deprecated when running edit without setup" do
+ assert_match "Encrypted secrets is deprecated", run_setup_command
+ assert_equal 1, $?.exitstatus
+ assert_not File.exist?("config/secrets.yml.enc")
+ end
+
+ test "encrypted secrets are deprecated for 5.1 config/secrets.yml apps" do
+ Dir.chdir(app_path) do
+ FileUtils.rm("config/credentials.yml.enc")
+ FileUtils.touch("config/secrets.yml")
+
+ assert_match "Encrypted secrets is deprecated", run_setup_command
+ assert_equal 1, $?.exitstatus
+ assert_not File.exist?("config/secrets.yml.enc")
+ end
end
test "edit secrets" do
- # Runs setup before first edit.
- assert_match(/Adding config\/secrets\.yml\.key to store the encryption key/, run_edit_command)
+ prevent_deprecation
# Run twice to ensure encrypted secrets can be reread after first edit pass.
2.times do
@@ -31,22 +48,30 @@ class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase
end
test "show secrets" do
- run_setup_command
+ prevent_deprecation
+
assert_match(/external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289/, run_show_command)
end
private
+ def prevent_deprecation
+ Dir.chdir(app_path) do
+ File.write("config/secrets.yml.key", "f731758c639da2604dfb6bf3d1025de8")
+ File.write("config/secrets.yml.enc", "sEB0mHxDbeP1/KdnMk00wyzPFACl9K6t0cZWn5/Mfx/YbTHvnI07vrneqHg9kaH3wOS7L6pIQteu1P077OtE4BSx/ZRc/sgQPHyWu/tXsrfHqnPNpayOF/XZqizE91JacSFItNMWpuPsp9ynbzz+7cGhoB1S4aPNIU6u0doMrzdngDbijsaAFJmsHIQh6t/QHoJx--8aMoE0PvUWmw1Iqz--ldFqnM/K0g9k17M8PKoN/Q==")
+ end
+ end
+
def run_edit_command(editor: "cat")
switch_env("EDITOR", editor) do
- rails "secrets:edit"
+ rails "secrets:edit", allow_failure: true
end
end
def run_show_command
- rails "secrets:show"
+ rails "secrets:show", allow_failure: true
end
def run_setup_command
- rails "secrets:setup"
+ rails "secrets:setup", allow_failure: true
end
end
diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb
index 556c2289e7..e7a56b3e6d 100644
--- a/railties/test/commands/server_test.rb
+++ b/railties/test/commands/server_test.rb
@@ -1,15 +1,15 @@
# frozen_string_literal: true
-require "abstract_unit"
+require "isolation/abstract_unit"
require "env_helpers"
require "rails/command"
require "rails/commands/server/server_command"
-class Rails::ServerTest < ActiveSupport::TestCase
+class Rails::Command::ServerCommandTest < ActiveSupport::TestCase
include EnvHelpers
def test_environment_with_server_option
- args = ["thin", "-e", "production"]
+ args = ["-u", "thin", "-e", "production"]
options = parse_arguments(args)
assert_equal "production", options[:environment]
assert_equal "thin", options[:server]
@@ -22,8 +22,38 @@ class Rails::ServerTest < ActiveSupport::TestCase
assert_nil options[:server]
end
+ def test_explicit_using_option
+ args = ["-u", "thin"]
+ options = parse_arguments(args)
+ assert_equal "thin", options[:server]
+ end
+
+ def test_using_server_mistype
+ assert_match(/Could not find server "tin". Maybe you meant "thin"?/, run_command("--using", "tin"))
+ end
+
+ def test_using_positional_argument_deprecation
+ assert_match(/DEPRECATION WARNING/, run_command("tin"))
+ end
+
+ def test_using_known_server_that_isnt_in_the_gemfile
+ assert_match(/Could not load server "unicorn". Maybe you need to the add it to the Gemfile/, run_command("-u", "unicorn"))
+ end
+
+ def test_daemon_with_option
+ args = ["-d"]
+ options = parse_arguments(args)
+ assert_equal true, options[:daemonize]
+ end
+
+ def test_daemon_without_option
+ args = []
+ options = parse_arguments(args)
+ assert_equal false, options[:daemonize]
+ end
+
def test_server_option_without_environment
- args = ["thin"]
+ args = ["-u", "thin"]
with_rack_env nil do
with_rails_env nil do
options = parse_arguments(args)
@@ -60,6 +90,15 @@ class Rails::ServerTest < ActiveSupport::TestCase
def test_environment_with_host
switch_env "HOST", "1.2.3.4" do
+ assert_deprecated do
+ options = parse_arguments
+ assert_equal "1.2.3.4", options[:Host]
+ end
+ end
+ end
+
+ def test_environment_with_binding
+ switch_env "BINDING", "1.2.3.4" do
options = parse_arguments
assert_equal "1.2.3.4", options[:Host]
end
@@ -81,6 +120,18 @@ class Rails::ServerTest < ActiveSupport::TestCase
assert_equal false, options[:caching]
end
+ def test_early_hints_with_option
+ args = ["--early-hints"]
+ options = parse_arguments(args)
+ assert_equal true, options[:early_hints]
+ end
+
+ def test_early_hints_is_nil_by_default
+ args = []
+ options = parse_arguments(args)
+ assert_nil options[:early_hints]
+ end
+
def test_log_stdout
with_rack_env nil do
with_rails_env nil do
@@ -154,7 +205,7 @@ class Rails::ServerTest < ActiveSupport::TestCase
assert_equal 3000, options[:Port]
end
- switch_env "HOST", "1.2.3.4" do
+ switch_env "BINDING", "1.2.3.4" do
args = ["-b", "127.0.0.1"]
options = parse_arguments(args)
assert_equal "127.0.0.1", options[:Host]
@@ -173,6 +224,11 @@ class Rails::ServerTest < ActiveSupport::TestCase
server_options = parse_arguments(["--port=3001"])
assert_equal [:Port], server_options[:user_supplied_options]
+
+ switch_env "BINDING", "1.2.3.4" do
+ server_options = parse_arguments
+ assert_equal [:Host], server_options[:user_supplied_options]
+ end
end
def test_default_options
@@ -197,7 +253,20 @@ class Rails::ServerTest < ActiveSupport::TestCase
ARGV.replace original_args
end
+ def test_served_url
+ args = %w(-u webrick -b 127.0.0.1 -p 4567)
+ server = Rails::Server.new(parse_arguments(args))
+ assert_equal "http://127.0.0.1:4567", server.served_url
+ end
+
private
+ def run_command(*args)
+ build_app
+ rails "server", *args
+ ensure
+ teardown_app
+ end
+
def parse_arguments(args = [])
Rails::Command::ServerCommand.new([], args).server_options
end
diff --git a/railties/test/engine/test_test.rb b/railties/test/engine/test_test.rb
new file mode 100644
index 0000000000..18af85a0aa
--- /dev/null
+++ b/railties/test/engine/test_test.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class Rails::Engine::TestTest < ActiveSupport::TestCase
+ setup do
+ @destination_root = Dir.mktmpdir("bukkits")
+ Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --mountable` }
+ end
+
+ teardown do
+ FileUtils.rm_rf(@destination_root)
+ end
+
+ test "automatically synchronize test schema" do
+ Dir.chdir(plugin_path) do
+ # In order to confirm that migration files are loaded, generate multiple migration files.
+ `bin/rails generate model user name:string;
+ bin/rails generate model todo name:string;
+ RAILS_ENV=development bin/rails db:migrate`
+
+ output = `bin/rails test test/models/bukkits/user_test.rb`
+ assert_includes(output, "0 runs, 0 assertions, 0 failures, 0 errors, 0 skips")
+ end
+ end
+
+ private
+ def plugin_path
+ "#{@destination_root}/bukkits"
+ end
+end
diff --git a/railties/test/engine_test.rb b/railties/test/engine_test.rb
index 4bd8a07085..19379e200c 100644
--- a/railties/test/engine_test.rb
+++ b/railties/test/engine_test.rb
@@ -11,7 +11,7 @@ class EngineTest < ActiveSupport::TestCase
end
end
- assert !engine.routes?
+ assert_not_predicate engine, :routes?
end
def test_application_can_be_subclassed
diff --git a/railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml b/railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml
deleted file mode 100644
index fe80872a16..0000000000
--- a/railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml
+++ /dev/null
@@ -1 +0,0 @@
-# an empty YAML file - any content in here seems to get parsed as a string \ No newline at end of file
diff --git a/railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb b/railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb
deleted file mode 100644
index 1a82a2bdd4..0000000000
--- a/railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# frozen_string_literal: true
-
-# intentionally empty
diff --git a/railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb b/railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb
deleted file mode 100644
index 1a82a2bdd4..0000000000
--- a/railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# frozen_string_literal: true
-
-# intentionally empty
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 3ecfb4edd9..f421207025 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -308,6 +308,14 @@ class ActionsTest < Rails::Generators::TestCase
end
end
+ test "rake command with capture option should run rake command with capture" do
+ assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=development", verbose: false, capture: true]) do
+ with_rails_env nil do
+ action :rake, "log:clear", capture: true
+ end
+ 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
@@ -346,6 +354,14 @@ class ActionsTest < Rails::Generators::TestCase
end
end
+ test "rails command with capture option should run rails_command with capture" do
+ assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=development", verbose: false, capture: true]) do
+ with_rails_env nil do
+ action :rails_command, "log:clear", capture: true
+ end
+ end
+ end
+
def test_capify_should_run_the_capify_command
content = capture(:stderr) do
assert_called_with(generator, :run, ["capify .", verbose: false]) do
diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb
index 7791d472d8..9c523ad372 100644
--- a/railties/test/generators/api_app_generator_test.rb
+++ b/railties/test/generators/api_app_generator_test.rb
@@ -13,7 +13,7 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
Rails.application = TestApp::Application
super
- Kernel::silence_warnings do
+ Kernel.silence_warnings do
Thor::Base.shell.send(:attr_accessor, :always_force)
@shell = Thor::Base.shell.new
@shell.send(:always_force=, true)
@@ -63,6 +63,23 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_generator_if_skip_action_mailer_is_given
+ run_generator [destination_root, "--api", "--skip-action-mailer"]
+ assert_file "config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/
+ assert_file "config/environments/development.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_file "config/environments/test.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_file "config/environments/production.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_no_directory "app/mailers"
+ assert_no_directory "test/mailers"
+ assert_no_directory "app/views"
+ end
+
def test_app_update_does_not_generate_unnecessary_config_files
run_generator
@@ -72,6 +89,7 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
assert_no_file "config/initializers/cookies_serializer.rb"
assert_no_file "config/initializers/assets.rb"
+ assert_no_file "config/initializers/content_security_policy.rb"
end
def test_app_update_does_not_generate_unnecessary_bin_files
@@ -149,6 +167,7 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
bin/yarn
config/initializers/assets.rb
config/initializers/cookies_serializer.rb
+ config/initializers/content_security_policy.rb
lib/assets
test/helpers
tmp/cache/assets
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index 904e2a5c84..a820d958f1 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -56,6 +56,7 @@ DEFAULT_APP_FILES = %w(
config/initializers/assets.rb
config/initializers/backtrace_silencers.rb
config/initializers/cookies_serializer.rb
+ config/initializers/content_security_policy.rb
config/initializers/filter_parameter_logging.rb
config/initializers/inflections.rb
config/initializers/mime_types.rb
@@ -75,6 +76,7 @@ DEFAULT_APP_FILES = %w(
log
package.json
public
+ storage
test/application_system_test_case.rb
test/test_helper.rb
test/fixtures
@@ -89,6 +91,7 @@ DEFAULT_APP_FILES = %w(
tmp
tmp/cache
tmp/cache/assets
+ tmp/storage
)
class AppGeneratorTest < Rails::Generators::TestCase
@@ -102,6 +105,15 @@ class AppGeneratorTest < Rails::Generators::TestCase
::DEFAULT_APP_FILES
end
+ def test_skip_bundle
+ assert_not_called(generator([destination_root], skip_bundle: true), :bundle_command) do
+ quietly { generator.invoke_all }
+ # skip_bundle is only about running bundle install, ensure the Gemfile is still
+ # generated.
+ assert_file "Gemfile"
+ end
+ end
+
def test_assets
run_generator
@@ -199,7 +211,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_new_application_doesnt_need_defaults
- assert_no_file "config/initializers/new_framework_defaults_5_2.rb"
+ assert_no_file "config/initializers/new_framework_defaults_6_0.rb"
end
def test_new_application_load_defaults
@@ -207,6 +219,8 @@ class AppGeneratorTest < Rails::Generators::TestCase
run_generator [app_root]
output = nil
+ assert_file "#{app_root}/config/application.rb", /\s+config\.load_defaults #{Rails::VERSION::STRING.to_f}/
+
Dir.chdir(app_root) do
output = `./bin/rails r "puts Rails.application.config.assets.unknown_asset_fallback"`
end
@@ -245,14 +259,14 @@ class AppGeneratorTest < Rails::Generators::TestCase
app_root = File.join(destination_root, "myapp")
run_generator [app_root]
- assert_no_file "#{app_root}/config/initializers/new_framework_defaults_5_2.rb"
+ assert_no_file "#{app_root}/config/initializers/new_framework_defaults_6_0.rb"
stub_rails_application(app_root) do
generator = Rails::Generators::AppGenerator.new ["rails"], { update: true }, { destination_root: app_root, shell: @shell }
generator.send(:app_const)
quietly { generator.send(:update_config_files) }
- assert_file "#{app_root}/config/initializers/new_framework_defaults_5_2.rb"
+ assert_file "#{app_root}/config/initializers/new_framework_defaults_6_0.rb"
end
end
@@ -296,6 +310,76 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_gem_for_active_storage
+ run_generator
+ assert_file "Gemfile", /^# gem 'image_processing'/
+ end
+
+ def test_gem_for_active_storage_when_skip_active_storage_is_given
+ run_generator [destination_root, "--skip-active-storage"]
+
+ assert_no_gem "image_processing"
+ end
+
+ def test_app_update_does_not_generate_active_storage_contents_when_skip_active_storage_is_given
+ app_root = File.join(destination_root, "myapp")
+ run_generator [app_root, "--skip-active-storage"]
+
+ FileUtils.cd(app_root) do
+ quietly { system("bin/rails app:update") }
+ end
+
+ assert_file "#{app_root}/config/environments/development.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{app_root}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{app_root}/config/environments/test.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_no_file "#{app_root}/config/storage.yml"
+ end
+
+ def test_app_update_does_not_generate_active_storage_contents_when_skip_active_record_is_given
+ app_root = File.join(destination_root, "myapp")
+ run_generator [app_root, "--skip-active-record"]
+
+ FileUtils.cd(app_root) do
+ quietly { system("bin/rails app:update") }
+ end
+
+ assert_file "#{app_root}/config/environments/development.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{app_root}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{app_root}/config/environments/test.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_no_file "#{app_root}/config/storage.yml"
+ end
+
+ def test_app_update_does_not_change_config_target_version
+ run_generator
+
+ FileUtils.cd(destination_root) do
+ config = "config/application.rb"
+ content = File.read(config)
+ File.write(config, content.gsub(/config\.load_defaults #{Rails::VERSION::STRING.to_f}/, "config.load_defaults 5.1"))
+ quietly { system("bin/rails app:update") }
+ end
+
+ assert_file "config/application.rb", /\s+config\.load_defaults 5\.1/
+ end
+
def test_application_names_are_not_singularized
run_generator [File.join(destination_root, "hats")]
assert_file "hats/config/environment.rb", /Rails\.application\.initialize!/
@@ -321,13 +405,13 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
- def test_config_another_database
+ def test_config_mysql_database
run_generator([destination_root, "-d", "mysql"])
assert_file "config/database.yml", /mysql/
if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcmysql-adapter"
else
- assert_gem "mysql2", "'>= 0.3.18', '< 0.5'"
+ assert_gem "mysql2", "'>= 0.4.4', '< 0.6.0'"
end
end
@@ -342,7 +426,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcpostgresql-adapter"
else
- assert_gem "pg", "'~> 0.18'"
+ assert_gem "pg", "'>= 0.18', '< 2.0'"
end
end
@@ -381,15 +465,13 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_generator_defaults_to_puma_version
run_generator [destination_root]
- assert_gem "puma", "'~> 3.7'"
+ assert_gem "puma", "'~> 3.11'"
end
def test_generator_if_skip_puma_is_given
run_generator [destination_root, "--skip-puma"]
assert_no_file "config/puma.rb"
- assert_file "Gemfile" do |content|
- assert_no_match(/puma/, content)
- end
+ assert_no_gem "puma"
end
def test_generator_has_assets_gems
@@ -409,20 +491,18 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/
- assert_file "Gemfile" do |content|
- assert_no_match(/capybara/, content)
- assert_no_match(/selenium-webdriver/, content)
- end
+ assert_no_gem "capybara"
+ assert_no_gem "selenium-webdriver"
+ assert_no_gem "chromedriver-helper"
assert_no_directory("test")
end
def test_generator_if_skip_system_test_is_given
run_generator [destination_root, "--skip-system-test"]
- assert_file "Gemfile" do |content|
- assert_no_match(/capybara/, content)
- assert_no_match(/selenium-webdriver/, content)
- end
+ assert_no_gem "capybara"
+ assert_no_gem "selenium-webdriver"
+ assert_no_gem "chromedriver-helper"
assert_directory("test")
@@ -468,10 +548,8 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_no_match(/javascript_include_tag\s+'application' \%>/, contents)
end
- assert_file "Gemfile" do |content|
- assert_no_match(/coffee-rails/, content)
- assert_no_match(/uglifier/, content)
- end
+ assert_no_gem "coffee-rails"
+ assert_no_gem "uglifier"
assert_file "config/environments/production.rb" do |content|
assert_no_match(/config\.assets\.js_compressor = :uglifier/, content)
@@ -495,9 +573,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_inclusion_of_a_debugger
run_generator
if defined?(JRUBY_VERSION) || RUBY_ENGINE == "rbx"
- assert_file "Gemfile" do |content|
- assert_no_match(/byebug/, content)
- end
+ assert_no_gem "byebug"
else
assert_gem "byebug"
end
@@ -560,6 +636,18 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_no_match(/run git init/, output)
end
+ def test_quiet_option
+ output = run_generator [File.join(destination_root, "myapp"), "--quiet"]
+ assert_empty output
+ end
+
+ def test_force_option_overwrites_every_file_except_master_key
+ run_generator [File.join(destination_root, "myapp")]
+ output = run_generator [File.join(destination_root, "myapp"), "--force"]
+ assert_match(/force/, output)
+ assert_no_match("force config/master.key", output)
+ end
+
def test_application_name_with_spaces
path = File.join(destination_root, "foo bar")
@@ -636,9 +724,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_called_with(Process, :respond_to?, [[:fork], [:fork]], returns: false) do
run_generator
- assert_file "Gemfile" do |content|
- assert_no_match(/spring/, content)
- end
+ assert_no_gem "spring"
end
end
@@ -646,25 +732,58 @@ class AppGeneratorTest < Rails::Generators::TestCase
run_generator [destination_root, "--skip-spring"]
assert_no_file "config/spring.rb"
- assert_file "Gemfile" do |content|
- assert_no_match(/spring/, content)
- end
+ assert_no_gem "spring"
end
def test_spring_with_dev_option
run_generator [destination_root, "--dev"]
- assert_file "Gemfile" do |content|
- assert_no_match(/spring/, content)
+ assert_no_gem "spring"
+ end
+
+ def test_webpack_option
+ command_check = -> command, *_ do
+ @called ||= 0
+ if command == "webpacker:install"
+ @called += 1
+ assert_equal 1, @called, "webpacker:install expected to be called once, but was called #{@called} times."
+ end
+ end
+
+ generator([destination_root], webpack: "webpack").stub(:rails_command, command_check) do
+ generator.stub :bundle_command, nil do
+ quietly { generator.invoke_all }
+ end
+ end
+
+ assert_gem "webpacker"
+ end
+
+ def test_webpack_option_with_js_framework
+ command_check = -> command, *_ do
+ case command
+ when "webpacker:install"
+ @webpacker ||= 0
+ @webpacker += 1
+ assert_equal 1, @webpacker, "webpacker:install expected to be called once, but was called #{@webpacker} times."
+ when "webpacker:install:react"
+ @react ||= 0
+ @react += 1
+ assert_equal 1, @react, "webpacker:install:react expected to be called once, but was called #{@react} times."
+ end
+ end
+
+ generator([destination_root], webpack: "react").stub(:rails_command, command_check) do
+ generator.stub :bundle_command, nil do
+ quietly { generator.invoke_all }
+ end
end
end
def test_generator_if_skip_turbolinks_is_given
run_generator [destination_root, "--skip-turbolinks"]
- assert_file "Gemfile" do |content|
- assert_no_match(/turbolinks/, content)
- end
+ assert_no_gem "turbolinks"
assert_file "app/views/layouts/application.html.erb" do |content|
assert_no_match(/data-turbolinks-track/, content)
end
@@ -673,6 +792,40 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_bootsnap
+ run_generator
+
+ unless defined?(JRUBY_VERSION)
+ assert_gem "bootsnap"
+ assert_file "config/boot.rb" do |content|
+ assert_match(/require 'bootsnap\/setup'/, content)
+ end
+ else
+ assert_no_gem "bootsnap"
+ assert_file "config/boot.rb" do |content|
+ assert_no_match(/require 'bootsnap\/setup'/, content)
+ end
+ end
+ end
+
+ def test_skip_bootsnap
+ run_generator [destination_root, "--skip-bootsnap"]
+
+ assert_no_gem "bootsnap"
+ assert_file "config/boot.rb" do |content|
+ assert_no_match(/require 'bootsnap\/setup'/, content)
+ end
+ end
+
+ def test_bootsnap_with_dev_option
+ run_generator [destination_root, "--dev"]
+
+ assert_no_gem "bootsnap"
+ assert_file "config/boot.rb" do |content|
+ assert_no_match(/require 'bootsnap\/setup'/, content)
+ end
+ end
+
def test_inclusion_of_ruby_version
run_generator
@@ -680,7 +833,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_match(/ruby '#{RUBY_VERSION}'/, content)
end
assert_file ".ruby-version" do |content|
- assert_match(/#{RUBY_VERSION}/, content)
+ assert_match(/#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}/, content)
end
end
@@ -737,7 +890,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
sequence = ["git init", "install", "exec spring binstub --all", "echo ran after_bundle"]
@sequence_step ||= 0
- ensure_bundler_first = -> command do
+ ensure_bundler_first = -> command, options = nil do
assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}"
@sequence_step += 1
end
@@ -745,7 +898,9 @@ class AppGeneratorTest < Rails::Generators::TestCase
generator([destination_root], template: path).stub(:open, check_open, template) do
generator.stub(:bundle_command, ensure_bundler_first) do
generator.stub(:run, ensure_bundler_first) do
- quietly { generator.invoke_all }
+ generator.stub(:rails_command, ensure_bundler_first) do
+ quietly { generator.invoke_all }
+ end
end
end
end
@@ -753,6 +908,14 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_equal 4, @sequence_step
end
+ def test_gitignore
+ run_generator
+
+ assert_file ".gitignore" do |content|
+ assert_match(/config\/master\.key/, content)
+ end
+ end
+
def test_system_tests_directory_generated
run_generator
@@ -760,6 +923,15 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_directory("test/system")
end
+ unless Gem.win_platform?
+ def test_master_key_is_only_readable_by_the_owner
+ run_generator
+
+ stat = File.stat("config/master.key")
+ assert_equal "100600", sprintf("%o", stat.mode)
+ end
+ end
+
private
def stub_rails_application(root)
Rails.application.config.root = root
@@ -780,6 +952,12 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
+ def assert_no_gem(gem)
+ assert_file "Gemfile" do |content|
+ assert_no_match(gem, content)
+ end
+ end
+
def assert_listen_related_configuration
assert_gem "listen"
assert_gem "spring-watcher-listen"
@@ -790,9 +968,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def assert_no_listen_related_configuration
- assert_file "Gemfile" do |content|
- assert_no_match(/listen/, content)
- end
+ assert_no_gem "listen"
assert_file "config/environments/development.rb" do |content|
assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content)
diff --git a/railties/test/generators/argv_scrubber_test.rb b/railties/test/generators/argv_scrubber_test.rb
index 3f4c985a33..9ef61dc978 100644
--- a/railties/test/generators/argv_scrubber_test.rb
+++ b/railties/test/generators/argv_scrubber_test.rb
@@ -82,9 +82,8 @@ module Rails
file.puts "--hello --world"
file.flush
- message = nil
scrubber = Class.new(ARGVScrubber) {
- define_method(:puts) { |msg| message = msg }
+ define_method(:puts) { |msg| }
}.new ["new", "--rc=#{file.path}"]
args = scrubber.prepare!
assert_equal ["--hello", "--world"], args
diff --git a/railties/test/generators/channel_generator_test.rb b/railties/test/generators/channel_generator_test.rb
index e238197eba..e543cc11b8 100644
--- a/railties/test/generators/channel_generator_test.rb
+++ b/railties/test/generators/channel_generator_test.rb
@@ -76,4 +76,14 @@ class ChannelGeneratorTest < Rails::Generators::TestCase
assert_file "app/channels/application_cable/connection.rb"
assert_file "app/assets/javascripts/cable.js"
end
+
+ def test_channel_suffix_is_not_duplicated
+ run_generator ["chat_channel"]
+
+ assert_no_file "app/channels/chat_channel_channel.rb"
+ assert_file "app/channels/chat_channel.rb"
+
+ assert_no_file "app/assets/javascripts/channels/chat_channel.js"
+ assert_file "app/assets/javascripts/channels/chat.js"
+ end
end
diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb
index a3218951a6..021004c9b8 100644
--- a/railties/test/generators/controller_generator_test.rb
+++ b/railties/test/generators/controller_generator_test.rb
@@ -109,4 +109,33 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
assert_match(/^ namespace :admin do\n get 'dashboard\/index'\n get 'dashboard\/show'\n end$/, route)
end
end
+
+ def test_does_not_add_routes_when_action_is_not_specified
+ run_generator ["admin/dashboard"]
+ assert_file "config/routes.rb" do |routes|
+ assert_no_match(/namespace :admin/, routes)
+ end
+ end
+
+ def test_controller_suffix_is_not_duplicated
+ run_generator ["account_controller"]
+
+ assert_no_file "app/controllers/account_controller_controller.rb"
+ assert_file "app/controllers/account_controller.rb"
+
+ assert_no_file "app/views/account_controller/"
+ assert_file "app/views/account/"
+
+ assert_no_file "test/controllers/account_controller_controller_test.rb"
+ assert_file "test/controllers/account_controller_test.rb"
+
+ assert_no_file "app/helpers/account_controller_helper.rb"
+ assert_file "app/helpers/account_helper.rb"
+
+ assert_no_file "app/assets/javascripts/account_controller.js"
+ assert_file "app/assets/javascripts/account.js"
+
+ assert_no_file "app/assets/stylesheets/account_controller.css"
+ assert_file "app/assets/stylesheets/account.css"
+ end
end
diff --git a/railties/test/generators/create_migration_test.rb b/railties/test/generators/create_migration_test.rb
index 3cb7fd6baa..2ae38045c5 100644
--- a/railties/test/generators/create_migration_test.rb
+++ b/railties/test/generators/create_migration_test.rb
@@ -70,7 +70,7 @@ class CreateMigrationTest < Rails::Generators::TestCase
create_migration
assert_match(/identical db\/migrate\/1_create_articles\.rb\n/, invoke!)
- assert @migration.identical?
+ assert_predicate @migration, :identical?
end
def test_invoke_when_exists_not_identical
diff --git a/railties/test/generators/encrypted_secrets_generator_test.rb b/railties/test/generators/encrypted_secrets_generator_test.rb
deleted file mode 100644
index eacb5166c0..0000000000
--- a/railties/test/generators/encrypted_secrets_generator_test.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require "generators/generators_test_helper"
-require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator"
-
-class EncryptedSecretsGeneratorTest < Rails::Generators::TestCase
- include GeneratorsTestHelper
-
- def setup
- super
- cd destination_root
- end
-
- def test_generates_key_file_and_encrypted_secrets_file
- run_generator
-
- assert_file "config/secrets.yml.key", /\w+/
-
- assert File.exist?("config/secrets.yml.enc")
- assert_no_match(/# production:\n# external_api_key: \w+/, IO.binread("config/secrets.yml.enc"))
- assert_match(/# production:\n# external_api_key: \w+/, Rails::Secrets.read)
- end
-
- def test_appends_to_gitignore
- FileUtils.touch(".gitignore")
-
- run_generator
-
- assert_file ".gitignore", /config\/secrets.yml.key/, /(?!config\/secrets.yml.enc)/
- end
-
- def test_warns_when_ignore_is_missing
- assert_match(/Add this to your ignore file/i, run_generator)
- end
-
- def test_doesnt_generate_a_new_key_file_if_already_opted_in_to_encrypted_secrets
- FileUtils.mkdir("config")
- File.open("config/secrets.yml.enc", "w") { |f| f.puts "already secrety" }
-
- run_generator
-
- assert_no_file "config/secrets.yml.key"
- end
-end
diff --git a/railties/test/generators/generated_attribute_test.rb b/railties/test/generators/generated_attribute_test.rb
index c6f7bdeda1..772b4f6f0d 100644
--- a/railties/test/generators/generated_attribute_test.rb
+++ b/railties/test/generators/generated_attribute_test.rb
@@ -104,25 +104,25 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
def test_reference_is_true
%w(references belongs_to).each do |attribute_type|
- assert create_generated_attribute(attribute_type).reference?
+ assert_predicate create_generated_attribute(attribute_type), :reference?
end
end
def test_reference_is_false
%w(foo bar baz).each do |attribute_type|
- assert !create_generated_attribute(attribute_type).reference?
+ assert_not_predicate create_generated_attribute(attribute_type), :reference?
end
end
def test_polymorphic_reference_is_true
%w(references belongs_to).each do |attribute_type|
- assert create_generated_attribute("#{attribute_type}{polymorphic}").polymorphic?
+ assert_predicate create_generated_attribute("#{attribute_type}{polymorphic}"), :polymorphic?
end
end
def test_polymorphic_reference_is_false
%w(foo bar baz).each do |attribute_type|
- assert !create_generated_attribute("#{attribute_type}{polymorphic}").polymorphic?
+ assert_not_predicate create_generated_attribute("#{attribute_type}{polymorphic}"), :polymorphic?
end
end
@@ -148,7 +148,7 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
att = Rails::Generators::GeneratedAttribute.parse("supplier:references{required}:index")
assert_equal "supplier", att.name
assert_equal :references, att.type
- assert att.has_index?
- assert att.required?
+ assert_predicate att, :has_index?
+ assert_predicate att, :required?
end
end
diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb
index cb5d8da7b1..ad2a55f496 100644
--- a/railties/test/generators/generators_test_helper.rb
+++ b/railties/test/generators/generators_test_helper.rb
@@ -43,9 +43,9 @@ module GeneratorsTestHelper
end
def copy_routes
- routes = File.expand_path("../../lib/rails/generators/rails/app/templates/config/routes.rb", __dir__)
+ routes = File.expand_path("../../lib/rails/generators/rails/app/templates/config/routes.rb.tt", __dir__)
destination = File.join(destination_root, "config")
FileUtils.mkdir_p(destination)
- FileUtils.cp routes, destination
+ FileUtils.cp routes, File.join(destination, "routes.rb")
end
end
diff --git a/railties/test/generators/job_generator_test.rb b/railties/test/generators/job_generator_test.rb
index 13276fac65..234ba6dad7 100644
--- a/railties/test/generators/job_generator_test.rb
+++ b/railties/test/generators/job_generator_test.rb
@@ -35,4 +35,14 @@ class JobGeneratorTest < Rails::Generators::TestCase
assert_match(/class ApplicationJob < ActiveJob::Base/, job)
end
end
+
+ def test_job_suffix_is_not_duplicated
+ run_generator ["notifier_job"]
+
+ assert_no_file "app/jobs/notifier_job_job.rb"
+ assert_file "app/jobs/notifier_job.rb"
+
+ assert_no_file "test/jobs/notifier_job_job_test.rb"
+ assert_file "test/jobs/notifier_job_test.rb"
+ end
end
diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb
index 516aa0704f..8d933e82c3 100644
--- a/railties/test/generators/model_generator_test.rb
+++ b/railties/test/generators/model_generator_test.rb
@@ -2,7 +2,6 @@
require "generators/generators_test_helper"
require "rails/generators/rails/model/model_generator"
-require "active_support/core_ext/string/strip"
class ModelGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -379,10 +378,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_required_belongs_to_adds_required_association
run_generator ["account", "supplier:references{required}"]
- expected_file = <<-FILE.strip_heredoc
- class Account < ApplicationRecord
- belongs_to :supplier, required: true
- end
+ expected_file = <<~FILE
+ class Account < ApplicationRecord
+ belongs_to :supplier, required: true
+ end
FILE
assert_file "app/models/account.rb", expected_file
end
@@ -390,10 +389,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_required_polymorphic_belongs_to_generages_correct_model
run_generator ["account", "supplier:references{required,polymorphic}"]
- expected_file = <<-FILE.strip_heredoc
- class Account < ApplicationRecord
- belongs_to :supplier, polymorphic: true, required: true
- end
+ expected_file = <<~FILE
+ class Account < ApplicationRecord
+ belongs_to :supplier, polymorphic: true, required: true
+ end
FILE
assert_file "app/models/account.rb", expected_file
end
@@ -401,10 +400,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_required_and_polymorphic_are_order_independent
run_generator ["account", "supplier:references{polymorphic.required}"]
- expected_file = <<-FILE.strip_heredoc
- class Account < ApplicationRecord
- belongs_to :supplier, polymorphic: true, required: true
- end
+ expected_file = <<~FILE
+ class Account < ApplicationRecord
+ belongs_to :supplier, polymorphic: true, required: true
+ end
FILE
assert_file "app/models/account.rb", expected_file
end
@@ -452,11 +451,11 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_token_option_adds_has_secure_token
run_generator ["user", "token:token", "auth_token:token"]
- expected_file = <<-FILE.strip_heredoc
- class User < ApplicationRecord
- has_secure_token
- has_secure_token :auth_token
- end
+ expected_file = <<~FILE
+ class User < ApplicationRecord
+ has_secure_token
+ has_secure_token :auth_token
+ end
FILE
assert_file "app/models/user.rb", expected_file
end
diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb
index 67f05926e3..4e61b660d7 100644
--- a/railties/test/generators/named_base_test.rb
+++ b/railties/test/generators/named_base_test.rb
@@ -33,6 +33,17 @@ class NamedBaseTest < Rails::Generators::TestCase
assert_name g, "foos", :plural_name
assert_name g, "admin.foo", :i18n_scope
assert_name g, "admin_foos", :table_name
+ assert_name g, "admin/foos", :controller_name
+ assert_name g, %w(admin), :controller_class_path
+ assert_name g, "Admin::Foos", :controller_class_name
+ assert_name g, "admin/foos", :controller_file_path
+ assert_name g, "foos", :controller_file_name
+ assert_name g, "admin.foos", :controller_i18n_scope
+ assert_name g, "admin_foo", :singular_route_name
+ assert_name g, "admin_foos", :plural_route_name
+ assert_name g, "@admin_foo", :redirect_resource_name
+ assert_name g, "admin_foo", :model_resource_name
+ assert_name g, "admin_foos", :index_helper
end
def test_named_generator_attributes_as_ruby
@@ -47,6 +58,17 @@ class NamedBaseTest < Rails::Generators::TestCase
assert_name g, "foos", :plural_name
assert_name g, "admin.foo", :i18n_scope
assert_name g, "admin_foos", :table_name
+ assert_name g, "Admin::Foos", :controller_name
+ assert_name g, %w(admin), :controller_class_path
+ assert_name g, "Admin::Foos", :controller_class_name
+ assert_name g, "admin/foos", :controller_file_path
+ assert_name g, "foos", :controller_file_name
+ assert_name g, "admin.foos", :controller_i18n_scope
+ assert_name g, "admin_foo", :singular_route_name
+ assert_name g, "admin_foos", :plural_route_name
+ assert_name g, "@admin_foo", :redirect_resource_name
+ assert_name g, "admin_foo", :model_resource_name
+ assert_name g, "admin_foos", :index_helper
end
def test_named_generator_attributes_without_pluralized
@@ -59,7 +81,7 @@ class NamedBaseTest < Rails::Generators::TestCase
ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names
end
- def test_scaffold_plural_names
+ def test_namespaced_scaffold_plural_names
g = generator ["admin/foo"]
assert_name g, "admin/foos", :controller_name
assert_name g, %w(admin), :controller_class_path
@@ -69,7 +91,7 @@ class NamedBaseTest < Rails::Generators::TestCase
assert_name g, "admin.foos", :controller_i18n_scope
end
- def test_scaffold_plural_names_as_ruby
+ def test_namespaced_scaffold_plural_names_as_ruby
g = generator ["Admin::Foo"]
assert_name g, "Admin::Foos", :controller_name
assert_name g, %w(admin), :controller_class_path
@@ -131,6 +153,19 @@ class NamedBaseTest < Rails::Generators::TestCase
assert_name g, "admin/foos", :controller_file_path
assert_name g, "foos", :controller_file_name
assert_name g, "admin.foos", :controller_i18n_scope
+ assert_name g, "admin_user", :singular_route_name
+ assert_name g, "admin_users", :plural_route_name
+ assert_name g, "[:admin, @user]", :redirect_resource_name
+ assert_name g, "[:admin, user]", :model_resource_name
+ assert_name g, "admin_users", :index_helper
+ end
+
+ def test_scaffold_plural_names
+ g = generator ["User"]
+ assert_name g, "@user", :redirect_resource_name
+ assert_name g, "user", :model_resource_name
+ assert_name g, "user", :singular_route_name
+ assert_name g, "users", :plural_route_name
end
private
diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb
index 38130ceb68..28ac3611b7 100644
--- a/railties/test/generators/plugin_generator_test.rb
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -82,11 +82,12 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
def test_generating_in_full_mode_with_almost_of_all_skip_options
- run_generator [destination_root, "--full", "-M", "-O", "-C", "-S", "-T"]
+ run_generator [destination_root, "--full", "-M", "-O", "-C", "-S", "-T", "--skip-active-storage"]
assert_file "bin/rails" do |content|
assert_no_match(/\s+require\s+["']rails\/all["']/, content)
end
assert_file "bin/rails", /#\s+require\s+["']active_record\/railtie["']/
+ assert_file "bin/rails", /#\s+require\s+["']active_storage\/engine["']/
assert_file "bin/rails", /#\s+require\s+["']action_mailer\/railtie["']/
assert_file "bin/rails", /#\s+require\s+["']action_cable\/engine["']/
assert_file "bin/rails", /#\s+require\s+["']sprockets\/railtie["']/
@@ -216,12 +217,22 @@ class PluginGeneratorTest < Rails::Generators::TestCase
def test_javascripts_generation
run_generator [destination_root, "--mountable"]
- assert_file "app/assets/javascripts/bukkits/application.js"
+ assert_file "app/assets/javascripts/bukkits/application.js" do |content|
+ assert_match "//= require rails-ujs", content
+ assert_match "//= require activestorage", content
+ assert_match "//= require_tree .", content
+ end
+ assert_file "app/views/layouts/bukkits/application.html.erb" do |content|
+ assert_match "javascript_include_tag", content
+ end
end
def test_skip_javascripts
run_generator [destination_root, "--skip-javascript", "--mountable"]
assert_no_file "app/assets/javascripts/bukkits/application.js"
+ assert_file "app/views/layouts/bukkits/application.html.erb" do |content|
+ assert_no_match "javascript_include_tag", content
+ end
end
def test_template_from_dir_pwd
@@ -240,8 +251,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
run_generator [destination_root, "--full", "--skip_active_record"]
FileUtils.cd destination_root
quietly { system "bundle install" }
- # FIXME: Active Storage will provoke a test error without ActiveRecord (fix by allowing to skip active storage)
- assert_match(/1 runs, 0 assertions, 0 failures, 1 errors/, `bundle exec rake test 2>&1`)
+ assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`)
end
def test_ensure_that_migration_tasks_work_with_mountable_option
@@ -321,8 +331,11 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "app/helpers/bukkits/application_helper.rb", /module Bukkits\n module ApplicationHelper/
assert_file "app/views/layouts/bukkits/application.html.erb" do |contents|
assert_match "<title>Bukkits</title>", contents
+ assert_match "<%= csrf_meta_tags %>", contents
+ assert_match "<%= csp_meta_tag %>", contents
assert_match(/stylesheet_link_tag\s+['"]bukkits\/application['"]/, contents)
assert_match(/javascript_include_tag\s+['"]bukkits\/application['"]/, contents)
+ assert_match "<%= yield %>", contents
end
assert_file "test/test_helper.rb" do |content|
assert_match(/ActiveRecord::Migrator\.migrations_paths.+\.\.\/test\/dummy\/db\/migrate/, content)
@@ -475,6 +488,8 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_no_file "test/dummy/Gemfile"
assert_no_file "test/dummy/public/robots.txt"
assert_no_file "test/dummy/README.md"
+ assert_no_file "test/dummy/config/master.key"
+ assert_no_file "test/dummy/config/credentials.yml.enc"
assert_no_directory "test/dummy/lib/tasks"
assert_no_directory "test/dummy/test"
assert_no_directory "test/dummy/vendor"
@@ -514,10 +529,11 @@ class PluginGeneratorTest < Rails::Generators::TestCase
gemfile_path = "#{Rails.root}/Gemfile"
Object.const_set("APP_PATH", Rails.root)
FileUtils.touch gemfile_path
+ File.write(gemfile_path, "#foo")
run_generator
- assert_file gemfile_path, /gem 'bukkits', path: 'tmp\/bukkits'/
+ assert_file gemfile_path, /^gem 'bukkits', path: 'tmp\/bukkits'/
ensure
Object.send(:remove_const, "APP_PATH")
FileUtils.rm gemfile_path
@@ -719,6 +735,38 @@ class PluginGeneratorTest < Rails::Generators::TestCase
Object.send(:remove_const, "ENGINE_ROOT")
end
+ def test_after_bundle_callback
+ path = "http://example.org/rails_template"
+ template = %{ after_bundle { run "echo ran after_bundle" } }.dup
+ template.instance_eval "def read; self; end" # Make the string respond to read
+
+ check_open = -> *args do
+ assert_equal [ path, "Accept" => "application/x-thor-template" ], args
+ template
+ end
+
+ sequence = ["echo ran after_bundle"]
+ @sequence_step ||= 0
+ ensure_bundler_first = -> command do
+ assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}"
+ @sequence_step += 1
+ end
+
+ content = nil
+ generator([destination_root], template: path).stub(:open, check_open, template) do
+ generator.stub(:bundle_command, ensure_bundler_first) do
+ generator.stub(:run, ensure_bundler_first) do
+ silence_stream($stdout) do
+ content = capture(:stderr) { generator.invoke_all }
+ end
+ end
+ end
+ end
+
+ assert_equal 1, @sequence_step
+ assert_match(/DEPRECATION WARNING: `after_bundle` is deprecated/, content)
+ end
+
private
def action(*args, &block)
diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb
index 384524aba9..fd5aa817b4 100644
--- a/railties/test/generators/scaffold_controller_generator_test.rb
+++ b/railties/test/generators/scaffold_controller_generator_test.rb
@@ -174,6 +174,29 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
assert_instance_method :index, content do |m|
assert_match("@users = User.all", m)
end
+
+ assert_instance_method :create, content do |m|
+ assert_match("redirect_to [:admin, @user]", m)
+ end
+
+ assert_instance_method :update, content do |m|
+ assert_match("redirect_to [:admin, @user]", m)
+ end
+ end
+
+ assert_file "app/views/admin/users/index.html.erb" do |content|
+ assert_match("'Show', [:admin, user]", content)
+ assert_match("'Edit', edit_admin_user_path(user)", content)
+ assert_match("'Destroy', [:admin, user]", content)
+ assert_match("'New User', new_admin_user_path", content)
+ end
+
+ assert_file "app/views/admin/users/new.html.erb" do |content|
+ assert_match("'Back', admin_users_path", content)
+ end
+
+ assert_file "app/views/admin/users/_form.html.erb" do |content|
+ assert_match("model: [:admin, user]", content)
end
end
@@ -184,6 +207,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
Dir.chdir(engine_path) do
quietly { `bin/rails g controller dashboard foo` }
+ quietly { `bin/rails db:migrate RAILS_ENV=test` }
assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`)
end
end
@@ -195,6 +219,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
Dir.chdir(engine_path) do
quietly { `bin/rails g controller dashboard foo` }
+ quietly { `bin/rails db:migrate RAILS_ENV=test` }
assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`)
end
end
diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb
index 03322c1c59..29426cd99f 100644
--- a/railties/test/generators/scaffold_generator_test.rb
+++ b/railties/test/generators/scaffold_generator_test.rb
@@ -282,7 +282,14 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
/class Admin::RolesTest < ApplicationSystemTestCase/
# Views
- %w(index edit new show _form).each do |view|
+ assert_file "app/views/admin/roles/index.html.erb" do |content|
+ assert_match("'Show', admin_role", content)
+ assert_match("'Edit', edit_admin_role_path(admin_role)", content)
+ assert_match("'Destroy', admin_role", content)
+ assert_match("'New Admin Role', new_admin_role_path", content)
+ end
+
+ %w(edit new show _form).each do |view|
assert_file "app/views/admin/roles/#{view}.html.erb"
end
assert_no_file "app/views/layouts/admin/roles.html.erb"
@@ -464,8 +471,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
assert_file "app/views/accounts/_form.html.erb" do |content|
- assert_match(/^\W{4}<%= form\.text_field :name, id: :account_name %>/, content)
- assert_match(/^\W{4}<%= form\.text_field :currency_id, id: :account_currency_id %>/, content)
+ assert_match(/^\W{4}<%= form\.text_field :name %>/, content)
+ assert_match(/^\W{4}<%= form\.text_field :currency_id %>/, content)
end
end
@@ -488,8 +495,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
assert_file "app/views/users/_form.html.erb" do |content|
- assert_match(/<%= form\.password_field :password, id: :user_password %>/, content)
- assert_match(/<%= form\.password_field :password_confirmation, id: :user_password_confirmation %>/, content)
+ assert_match(/<%= form\.password_field :password %>/, content)
+ assert_match(/<%= form\.password_field :password_confirmation %>/, content)
end
assert_file "app/views/users/index.html.erb" do |content|
diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb
index 654d16de65..aa577e4234 100644
--- a/railties/test/generators/shared_generator_tests.rb
+++ b/railties/test/generators/shared_generator_tests.rb
@@ -9,7 +9,7 @@ module SharedGeneratorTests
super
Rails::Generators::AppGenerator.instance_variable_set("@desc", nil)
- Kernel::silence_warnings do
+ Kernel.silence_warnings do
Thor::Base.shell.send(:attr_accessor, :always_force)
@shell = Thor::Base.shell.new
@shell.send(:always_force=, true)
@@ -92,7 +92,9 @@ module SharedGeneratorTests
end
generator([destination_root], template: path).stub(:open, check_open, template) do
- quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) }
+ generator.stub :bundle_command, nil do
+ quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) }
+ end
end
end
@@ -103,15 +105,6 @@ module SharedGeneratorTests
end
end
- def test_skip_bundle
- assert_not_called(generator([destination_root], skip_bundle: true), :bundle_command) do
- quietly { generator.invoke_all }
- # skip_bundle is only about running bundle install, ensure the Gemfile is still
- # generated.
- assert_file "Gemfile"
- end
- end
-
def test_skip_git
run_generator [destination_root, "--skip-git", "--full"]
assert_no_file(".gitignore")
@@ -129,13 +122,26 @@ module SharedGeneratorTests
end
def test_default_frameworks_are_required_when_others_are_removed
- run_generator [destination_root, "--skip-active-record", "--skip-action-mailer", "--skip-action-cable", "--skip-sprockets"]
- assert_file "#{application_path}/config/application.rb", /require\s+["']rails["']/
- assert_file "#{application_path}/config/application.rb", /require\s+["']active_model\/railtie["']/
- assert_file "#{application_path}/config/application.rb", /require\s+["']active_job\/railtie["']/
- assert_file "#{application_path}/config/application.rb", /require\s+["']action_controller\/railtie["']/
- assert_file "#{application_path}/config/application.rb", /require\s+["']action_view\/railtie["']/
- assert_file "#{application_path}/config/application.rb", /require\s+["']active_storage\/engine["']/
+ run_generator [
+ destination_root,
+ "--skip-active-record",
+ "--skip-active-storage",
+ "--skip-action-mailer",
+ "--skip-action-cable",
+ "--skip-sprockets"
+ ]
+
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']rails["']/
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']active_model\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']active_job\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']active_record\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']active_storage\/engine["']/
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']action_controller\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']action_mailer\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']action_view\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']action_cable\/engine["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']sprockets\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']rails\/test_unit\/railtie["']/
end
def test_generator_without_skips
@@ -189,6 +195,94 @@ module SharedGeneratorTests
end
end
+ def test_generator_for_active_storage
+ run_generator
+
+ assert_file "#{application_path}/app/assets/javascripts/application.js" do |content|
+ assert_match(/^\/\/= require activestorage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/development.rb" do |content|
+ assert_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/production.rb" do |content|
+ assert_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/test.rb" do |content|
+ assert_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/storage.yml"
+ assert_directory "#{application_path}/storage"
+ assert_directory "#{application_path}/tmp/storage"
+
+ assert_file ".gitignore" do |content|
+ assert_match(/\/storage\//, content)
+ end
+ end
+
+ def test_generator_if_skip_active_storage_is_given
+ run_generator [destination_root, "--skip-active-storage"]
+
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_storage\/engine["']/
+
+ assert_file "#{application_path}/app/assets/javascripts/application.js" do |content|
+ assert_no_match(/^\/\/= require activestorage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/development.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/test.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_no_file "#{application_path}/config/storage.yml"
+ assert_no_directory "#{application_path}/storage"
+ assert_no_directory "#{application_path}/tmp/storage"
+
+ assert_file ".gitignore" do |content|
+ assert_no_match(/\/storage\//, content)
+ end
+ end
+
+ def test_generator_does_not_generate_active_storage_contents_if_skip_active_record_is_given
+ run_generator [destination_root, "--skip-active-record"]
+
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_storage\/engine["']/
+
+ assert_file "#{application_path}/app/assets/javascripts/application.js" do |content|
+ assert_no_match(/^\/\/= require activestorage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/development.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/test.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_no_file "#{application_path}/config/storage.yml"
+ assert_no_directory "#{application_path}/storage"
+ assert_no_directory "#{application_path}/tmp/storage"
+
+ assert_file ".gitignore" do |content|
+ assert_no_match(/\/storage\//, content)
+ end
+ end
+
def test_generator_if_skip_action_mailer_is_given
run_generator [destination_root, "--skip-action-mailer"]
assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/
diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb
index 28e7617d7f..a16a2d3f0a 100644
--- a/railties/test/generators_test.rb
+++ b/railties/test/generators_test.rb
@@ -33,13 +33,26 @@ class GeneratorsTest < Rails::Generators::TestCase
def test_generator_suggestions
name = :migrationz
output = capture(:stdout) { Rails::Generators.invoke name }
- assert_match "Maybe you meant 'migration'", output
+ assert_match 'Maybe you meant "migration"?', output
+ end
+
+ def test_generator_suggestions_except_en_locale
+ orig_available_locales = I18n.available_locales
+ orig_default_locale = I18n.default_locale
+ I18n.available_locales = :ja
+ I18n.default_locale = :ja
+ name = :tas
+ output = capture(:stdout) { Rails::Generators.invoke name }
+ assert_match 'Maybe you meant "task"?', output
+ ensure
+ I18n.available_locales = orig_available_locales
+ I18n.default_locale = orig_default_locale
end
def test_generator_multiple_suggestions
name = :tas
output = capture(:stdout) { Rails::Generators.invoke name }
- assert_match "Maybe you meant 'task', 'job' or", output
+ assert_match 'Maybe you meant "task"?', output
end
def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments
@@ -51,7 +64,7 @@ class GeneratorsTest < Rails::Generators::TestCase
assert File.exist?(File.join(@path, "generators", "model_generator.rb"))
assert_called_with(Rails::Generators::ModelGenerator, :start, [["Account"], {}]) do
warnings = capture(:stderr) { Rails::Generators.invoke :model, ["Account"] }
- assert warnings.empty?
+ assert_empty warnings
end
end
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index b7f214cb73..3cde7c03b0 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -38,7 +38,12 @@ module TestHelpers
end
def app_path(*args)
- tmp_path(*%w[app] + args)
+ path = tmp_path(*%w[app] + args)
+ if block_given?
+ yield path
+ else
+ path
+ end
end
def framework_path
@@ -82,22 +87,6 @@ module TestHelpers
assert_match "charset=utf-8", resp[1]["Content-Type"]
assert extract_body(resp).match(/Yay! You.*re on Rails!/)
end
-
- def assert_success(resp)
- assert_equal 202, resp[0]
- end
-
- def assert_missing(resp)
- assert_equal 404, resp[0]
- end
-
- def assert_header(key, value, resp)
- assert_equal value, resp[1][key.to_s]
- end
-
- def assert_body(expected, resp)
- assert_equal expected, extract_body(resp)
- end
end
module Generation
@@ -123,22 +112,57 @@ module TestHelpers
end
end
- File.open("#{app_path}/config/database.yml", "w") do |f|
- f.puts <<-YAML
- default: &default
- adapter: sqlite3
- pool: 5
- timeout: 5000
- development:
- <<: *default
- database: db/development.sqlite3
- test:
- <<: *default
- database: db/test.sqlite3
- production:
- <<: *default
- database: db/production.sqlite3
- YAML
+ if options[:multi_db]
+ File.open("#{app_path}/config/database.yml", "w") do |f|
+ f.puts <<-YAML
+ default: &default
+ adapter: sqlite3
+ pool: 5
+ timeout: 5000
+ development:
+ primary:
+ <<: *default
+ database: db/development.sqlite3
+ animals:
+ <<: *default
+ database: db/development_animals.sqlite3
+ migrations_paths: db/animals_migrate
+ test:
+ primary:
+ <<: *default
+ database: db/test.sqlite3
+ animals:
+ <<: *default
+ database: db/test_animals.sqlite3
+ migrations_paths: db/animals_migrate
+ production:
+ primary:
+ <<: *default
+ database: db/production.sqlite3
+ animals:
+ <<: *default
+ database: db/production_animals.sqlite3
+ migrations_paths: db/animals_migrate
+ YAML
+ end
+ else
+ File.open("#{app_path}/config/database.yml", "w") do |f|
+ f.puts <<-YAML
+ default: &default
+ adapter: sqlite3
+ pool: 5
+ timeout: 5000
+ development:
+ <<: *default
+ database: db/development.sqlite3
+ test:
+ <<: *default
+ database: db/test.sqlite3
+ production:
+ <<: *default
+ database: db/production.sqlite3
+ YAML
+ end
end
add_to_config <<-RUBY
@@ -153,6 +177,7 @@ module TestHelpers
def teardown_app
ENV["RAILS_ENV"] = @prev_rails_env if @prev_rails_env
+ FileUtils.rm_rf(tmp_path)
end
# Make a very basic app, without creating the whole directory structure.
@@ -235,7 +260,7 @@ module TestHelpers
# Invoke a bin/rails command inside the app
#
- # allow_failures:: true to return normally if the command exits with
+ # allow_failure:: true to return normally if the command exits with
# a non-zero status. By default, this method will raise.
# stderr:: true to pass STDERR output straight to the "real" STDERR.
# By default, the STDERR and STDOUT of the process will be
@@ -357,10 +382,12 @@ module TestHelpers
end
def app_file(path, contents, mode = "w")
- FileUtils.mkdir_p File.dirname("#{app_path}/#{path}")
- File.open("#{app_path}/#{path}", mode) do |f|
+ file_name = "#{app_path}/#{path}"
+ FileUtils.mkdir_p File.dirname(file_name)
+ File.open(file_name, mode) do |f|
f.puts contents
end
+ file_name
end
def remove_file(path)
@@ -380,6 +407,21 @@ module TestHelpers
$:.reject! { |path| path =~ %r'/(#{to_remove.join('|')})/' }
end
+
+ def use_postgresql
+ File.open("#{app_path}/config/database.yml", "w") do |f|
+ f.puts <<-YAML
+ default: &default
+ adapter: postgresql
+ pool: 5
+ database: railties_test
+ development:
+ <<: *default
+ test:
+ <<: *default
+ YAML
+ end
+ end
end
end
@@ -388,6 +430,10 @@ class ActiveSupport::TestCase
include TestHelpers::Rack
include TestHelpers::Generation
include ActiveSupport::Testing::Stream
+
+ def frozen_error_class
+ Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError
+ end
end
# Create a scope and build a fixture rails app
diff --git a/railties/test/minitest/rails_plugin_test.rb b/railties/test/minitest/rails_plugin_test.rb
new file mode 100644
index 0000000000..7c3a2022a9
--- /dev/null
+++ b/railties/test/minitest/rails_plugin_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class Minitest::RailsPluginTest < ActiveSupport::TestCase
+ setup do
+ @options = Minitest.process_args []
+ @output = StringIO.new("".encode("UTF-8"))
+ end
+
+ test "default reporters are replaced" do
+ with_reporter Minitest::CompositeReporter.new do |reporter|
+ reporter << Minitest::SummaryReporter.new(@output, @options)
+ reporter << Minitest::ProgressReporter.new(@output, @options)
+ reporter << Minitest::Reporter.new(@output, @options)
+
+ Minitest.plugin_rails_init({})
+
+ assert_equal 3, reporter.reporters.count
+ assert reporter.reporters.any? { |candidate| candidate.kind_of?(Minitest::SuppressedSummaryReporter) }
+ assert reporter.reporters.any? { |candidate| candidate.kind_of?(::Rails::TestUnitReporter) }
+ assert reporter.reporters.any? { |candidate| candidate.kind_of?(Minitest::Reporter) }
+ end
+ end
+
+ test "no custom reporters are added if nothing to replace" do
+ with_reporter Minitest::CompositeReporter.new do |reporter|
+ Minitest.plugin_rails_init({})
+
+ assert_empty reporter.reporters
+ end
+ end
+
+ private
+ def with_reporter(reporter)
+ old_reporter, Minitest.reporter = Minitest.reporter, reporter
+
+ yield reporter
+ ensure
+ Minitest.reporter = old_reporter
+ end
+end
diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb
index 854b4448a4..9f5bb37c20 100644
--- a/railties/test/paths_test.rb
+++ b/railties/test/paths_test.rb
@@ -104,7 +104,7 @@ class PathsTest < ActiveSupport::TestCase
File.stub(:exist?, true) do
@root.add "app", with: "/app"
@root["app"].autoload_once!
- assert @root["app"].autoload_once?
+ assert_predicate @root["app"], :autoload_once?
assert_includes @root.autoload_once, @root["app"].expanded.first
end
end
@@ -112,17 +112,17 @@ class PathsTest < ActiveSupport::TestCase
test "it is possible to remove a path that should be autoloaded only once" do
@root["app"] = "/app"
@root["app"].autoload_once!
- assert @root["app"].autoload_once?
+ assert_predicate @root["app"], :autoload_once?
@root["app"].skip_autoload_once!
- assert !@root["app"].autoload_once?
+ assert_not_predicate @root["app"], :autoload_once?
assert_not_includes @root.autoload_once, @root["app"].expanded.first
end
test "it is possible to add a path without assignment and specify it should be loaded only once" do
File.stub(:exist?, true) do
@root.add "app", with: "/app", autoload_once: true
- assert @root["app"].autoload_once?
+ assert_predicate @root["app"], :autoload_once?
assert_includes @root.autoload_once, "/app"
end
end
@@ -130,7 +130,7 @@ class PathsTest < ActiveSupport::TestCase
test "it is possible to add multiple paths without assignment and specify it should be loaded only once" do
File.stub(:exist?, true) do
@root.add "app", with: ["/app", "/app2"], autoload_once: true
- assert @root["app"].autoload_once?
+ assert_predicate @root["app"], :autoload_once?
assert_includes @root.autoload_once, "/app"
assert_includes @root.autoload_once, "/app2"
end
@@ -158,7 +158,7 @@ class PathsTest < ActiveSupport::TestCase
File.stub(:exist?, true) do
@root["app"] = "/app"
@root["app"].eager_load!
- assert @root["app"].eager_load?
+ assert_predicate @root["app"], :eager_load?
assert_includes @root.eager_load, @root["app"].to_a.first
end
end
@@ -166,17 +166,17 @@ class PathsTest < ActiveSupport::TestCase
test "it is possible to skip a path from eager loading" do
@root["app"] = "/app"
@root["app"].eager_load!
- assert @root["app"].eager_load?
+ assert_predicate @root["app"], :eager_load?
@root["app"].skip_eager_load!
- assert !@root["app"].eager_load?
+ assert_not_predicate @root["app"], :eager_load?
assert_not_includes @root.eager_load, @root["app"].to_a.first
end
test "it is possible to add a path without assignment and mark it as eager" do
File.stub(:exist?, true) do
@root.add "app", with: "/app", eager_load: true
- assert @root["app"].eager_load?
+ assert_predicate @root["app"], :eager_load?
assert_includes @root.eager_load, "/app"
end
end
@@ -184,7 +184,7 @@ class PathsTest < ActiveSupport::TestCase
test "it is possible to add multiple paths without assignment and mark them as eager" do
File.stub(:exist?, true) do
@root.add "app", with: ["/app", "/app2"], eager_load: true
- assert @root["app"].eager_load?
+ assert_predicate @root["app"], :eager_load?
assert_includes @root.eager_load, "/app"
assert_includes @root.eager_load, "/app2"
end
@@ -193,8 +193,8 @@ class PathsTest < ActiveSupport::TestCase
test "it is possible to create a path without assignment and mark it both as eager and load once" do
File.stub(:exist?, true) do
@root.add "app", with: "/app", eager_load: true, autoload_once: true
- assert @root["app"].eager_load?
- assert @root["app"].autoload_once?
+ assert_predicate @root["app"], :eager_load?
+ assert_predicate @root["app"], :autoload_once?
assert_includes @root.eager_load, "/app"
assert_includes @root.autoload_once, "/app"
end
@@ -254,7 +254,7 @@ class PathsTest < ActiveSupport::TestCase
test "a path can be added to the load path on creation" do
File.stub(:exist?, true) do
@root.add "app", with: "/app", load_path: true
- assert @root["app"].load_path?
+ assert_predicate @root["app"], :load_path?
assert_equal ["/app"], @root.load_paths
end
end
@@ -271,7 +271,7 @@ class PathsTest < ActiveSupport::TestCase
test "a path can be marked as autoload on creation" do
File.stub(:exist?, true) do
@root.add "app", with: "/app", autoload: true
- assert @root["app"].autoload?
+ assert_predicate @root["app"], :autoload?
assert_equal ["/app"], @root.autoload_paths
end
end
diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb
index e47f30d5b6..6e8f333e1d 100644
--- a/railties/test/rack_logger_test.rb
+++ b/railties/test/rack_logger_test.rb
@@ -14,14 +14,21 @@ module Rails
attr_reader :logger
- def initialize(logger = NULL, taggers = nil, &block)
- super(->(_) { block.call; [200, {}, []] }, taggers)
+ def initialize(logger = NULL, app: nil, taggers: nil, &block)
+ app ||= ->(_) { block.call; [200, {}, []] }
+ super(app, taggers)
@logger = logger
end
def development?; false; end
end
+ class TestApp < Struct.new(:response)
+ def call(_env)
+ response
+ end
+ end
+
Subscriber = Struct.new(:starts, :finishes) do
def initialize(starts = [], finishes = [])
super
@@ -72,6 +79,17 @@ module Rails
end
end
end
+
+ def test_logger_does_not_mutate_app_return
+ response = [].freeze
+ app = TestApp.new(response)
+ logger = TestLogger.new(app: app)
+ assert_no_changes("response") do
+ assert_nothing_raised do
+ logger.call("REQUEST_METHOD" => "GET")
+ end
+ end
+ end
end
end
end
diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb
index 227a739b71..50522c1be6 100644
--- a/railties/test/rails_info_test.rb
+++ b/railties/test/rails_info_test.rb
@@ -2,27 +2,14 @@
require "abstract_unit"
-unless defined?(Rails) && defined?(Rails::Info)
- module Rails
- class Info; end
- end
-end
-
-require "active_support/core_ext/kernel/reporting"
-
class InfoTest < ActiveSupport::TestCase
- def setup
- Rails.send :remove_const, :Info
- silence_warnings { load "rails/info.rb" }
- end
-
def test_property_with_block_swallows_exceptions_and_ignores_property
assert_nothing_raised do
Rails::Info.module_eval do
property("Bogus") { raise }
end
end
- assert !property_defined?("Bogus")
+ assert_not property_defined?("Bogus")
end
def test_property_with_string
diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb
index e6964b4b18..9a3ddc8d5e 100644
--- a/railties/test/railties/engine_test.rb
+++ b/railties/test/railties/engine_test.rb
@@ -32,6 +32,11 @@ module RailtiesTest
require "#{app_path}/config/environment"
end
+ def migrations
+ migration_root = File.expand_path(ActiveRecord::Migrator.migrations_paths.first, app_path)
+ ActiveRecord::MigrationContext.new(migration_root).migrations
+ end
+
test "serving sprocket's assets" do
@plugin.write "app/assets/javascripts/engine.js.erb", "<%= :alert %>();"
add_to_env_config "development", "config.assets.digest = false"
@@ -82,31 +87,32 @@ module RailtiesTest
end
RUBY
- add_to_config "ActiveRecord::Base.timestamped_migrations = false"
-
boot_rails
Dir.chdir(app_path) do
+ # Install Active Storage migration file first so as not to affect test.
+ `bundle exec rake active_storage:install`
output = `bundle exec rake bukkits:install:migrations`
- assert File.exist?("#{app_path}/db/migrate/2_create_users.bukkits.rb")
- assert File.exist?("#{app_path}/db/migrate/3_add_last_name_to_users.bukkits.rb")
- assert_match(/Copied migration 2_create_users\.bukkits\.rb from bukkits/, output)
- assert_match(/Copied migration 3_add_last_name_to_users\.bukkits\.rb from bukkits/, output)
- assert_match(/NOTE: Migration 3_create_sessions\.rb from bukkits has been skipped/, output)
- assert_equal 3, Dir["#{app_path}/db/migrate/*.rb"].length
-
- output = `bundle exec rake railties:install:migrations`.split("\n")
+ ["CreateUsers", "AddLastNameToUsers", "CreateSessions"].each do |migration_name|
+ assert migrations.detect { |migration| migration.name == migration_name }
+ end
+ assert_match(/Copied migration \d+_create_users\.bukkits\.rb from bukkits/, output)
+ assert_match(/Copied migration \d+_add_last_name_to_users\.bukkits\.rb from bukkits/, output)
+ assert_match(/NOTE: Migration \d+_create_sessions\.rb from bukkits has been skipped/, output)
- assert_no_match(/2_create_users/, output.join("\n"))
+ migrations_count = Dir["#{app_path}/db/migrate/*.rb"].length
- bukkits_migration_order = output.index(output.detect { |o| /NOTE: Migration 3_create_sessions\.rb from bukkits has been skipped/ =~ o })
- assert_not_nil bukkits_migration_order, "Expected migration to be skipped"
+ assert_equal migrations.length, migrations_count
- migrations_count = Dir["#{app_path}/db/migrate/*.rb"].length
- `bundle exec rake railties:install:migrations`
+ output = `bundle exec rake railties:install:migrations`.split("\n")
assert_equal migrations_count, Dir["#{app_path}/db/migrate/*.rb"].length
+
+ assert_no_match(/\d+_create_users/, output.join("\n"))
+
+ bukkits_migration_order = output.index(output.detect { |o| /NOTE: Migration \d+_create_sessions\.rb from bukkits has been skipped/ =~ o })
+ assert_not_nil bukkits_migration_order, "Expected migration to be skipped"
end
end
@@ -171,10 +177,12 @@ module RailtiesTest
boot_rails
Dir.chdir(app_path) do
+ # Install Active Storage migration file first so as not to affect test.
+ `bundle exec rake active_storage:install`
output = `bundle exec rake railties:install:migrations`.split("\n")
- assert_match(/Copied migration \d+_create_users\.core_engine\.rb from core_engine/, output.second)
- assert_match(/Copied migration \d+_create_keys\.api_engine\.rb from api_engine/, output.last)
+ assert_match(/Copied migration \d+_create_users\.core_engine\.rb from core_engine/, output.first)
+ assert_match(/Copied migration \d+_create_keys\.api_engine\.rb from api_engine/, output.second)
end
end
@@ -203,9 +211,12 @@ module RailtiesTest
Dir.chdir(@plugin.path) do
output = `bundle exec rake app:bukkits:install:migrations`
- assert File.exist?("#{app_path}/db/migrate/0_add_first_name_to_users.bukkits.rb")
- assert_match(/Copied migration 0_add_first_name_to_users\.bukkits\.rb from bukkits/, output)
- assert_equal 1, Dir["#{app_path}/db/migrate/*.rb"].length
+
+ migration_with_engine_path = migrations.detect { |migration| migration.name == "AddFirstNameToUsers" }
+ assert migration_with_engine_path
+ assert_match(/\/db\/migrate\/\d+_add_first_name_to_users\.bukkits\.rb/, migration_with_engine_path.filename)
+ assert_match(/Copied migration \d+_add_first_name_to_users\.bukkits\.rb from bukkits/, output)
+ assert_equal migrations.length, Dir["#{app_path}/db/migrate/*.rb"].length
end
end
@@ -215,7 +226,7 @@ module RailtiesTest
require "rdoc/task"
require "rake/testtask"
Rails.application.load_tasks
- assert !Rake::Task.task_defined?("bukkits:install:migrations")
+ assert_not Rake::Task.task_defined?("bukkits:install:migrations")
end
test "puts its lib directory on load path" do
@@ -734,7 +745,7 @@ YAML
assert_equal "bukkits", Bukkits::Engine.engine_name
assert_equal Bukkits.railtie_namespace, Bukkits::Engine
assert ::Bukkits::MyMailer.method_defined?(:foo_url)
- assert !::Bukkits::MyMailer.method_defined?(:bar_url)
+ assert_not ::Bukkits::MyMailer.method_defined?(:bar_url)
get("/bukkits/from_app")
assert_equal "false", last_response.body
@@ -969,14 +980,14 @@ YAML
boot_rails
app_generators = Rails.application.config.generators.options[:rails]
- assert_equal :mongoid , app_generators[:orm]
- assert_equal :liquid , app_generators[:template_engine]
+ assert_equal :mongoid, app_generators[:orm]
+ assert_equal :liquid, app_generators[:template_engine]
assert_equal :test_unit, app_generators[:test_framework]
generators = Bukkits::Engine.config.generators.options[:rails]
assert_equal :data_mapper, generators[:orm]
- assert_equal :haml , generators[:template_engine]
- assert_equal :rspec , generators[:test_framework]
+ assert_equal :haml, generators[:template_engine]
+ assert_equal :rspec, generators[:test_framework]
end
test "engine should get default generators with ability to overwrite them" do
@@ -992,10 +1003,10 @@ YAML
generators = Bukkits::Engine.config.generators.options[:rails]
assert_equal :active_record, generators[:orm]
- assert_equal :rspec , generators[:test_framework]
+ assert_equal :rspec, generators[:test_framework]
app_generators = Rails.application.config.generators.options[:rails]
- assert_equal :test_unit , app_generators[:test_framework]
+ assert_equal :test_unit, app_generators[:test_framework]
end
test "do not create table_name_prefix method if it already exists" do
@@ -1468,6 +1479,21 @@ YAML
assert_equal "/fruits/2/bukkits/posts", last_response.body
end
+ test "active_storage:install task works within engine" do
+ @plugin.write "Rakefile", <<-RUBY
+ APP_RAKEFILE = '#{app_path}/Rakefile'
+ load 'rails/tasks/engine.rake'
+ RUBY
+
+ Dir.chdir(@plugin.path) do
+ output = `bundle exec rake app:active_storage:install`
+ assert $?.success?, output
+
+ active_storage_migration = migrations.detect { |migration| migration.name == "CreateActiveStorageTables" }
+ assert active_storage_migration
+ end
+ end
+
private
def app
Rails.application
diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb
index 359ab0fdae..7c3d1e3759 100644
--- a/railties/test/railties/railtie_test.rb
+++ b/railties/test/railties/railtie_test.rb
@@ -65,7 +65,7 @@ module RailtiesTest
test "railtie can add to_prepare callbacks" do
$to_prepare = false
class Foo < Rails::Railtie ; config.to_prepare { $to_prepare = true } ; end
- assert !$to_prepare
+ assert_not $to_prepare
require "#{app_path}/config/environment"
require "rack/test"
extend Rack::Test::Methods
@@ -91,7 +91,7 @@ module RailtiesTest
test "railtie can add after_initialize callbacks" do
$after_initialize = false
class Foo < Rails::Railtie ; config.after_initialize { $after_initialize = true } ; end
- assert !$after_initialize
+ assert_not $after_initialize
require "#{app_path}/config/environment"
assert $after_initialize
end
@@ -107,7 +107,7 @@ module RailtiesTest
require "#{app_path}/config/environment"
- assert !$ran_block
+ assert_not $ran_block
require "rake"
require "rake/testtask"
require "rdoc/task"
@@ -151,7 +151,7 @@ module RailtiesTest
require "#{app_path}/config/environment"
- assert !$ran_block
+ assert_not $ran_block
Rails.application.load_generators
assert $ran_block
end
@@ -167,7 +167,7 @@ module RailtiesTest
require "#{app_path}/config/environment"
- assert !$ran_block
+ assert_not $ran_block
Rails.application.load_console
assert $ran_block
end
@@ -183,7 +183,7 @@ module RailtiesTest
require "#{app_path}/config/environment"
- assert !$ran_block
+ assert_not $ran_block
Rails.application.load_runner
assert $ran_block
end
@@ -197,7 +197,7 @@ module RailtiesTest
end
end
- assert !$ran_block
+ assert_not $ran_block
require "#{app_path}/config/environment"
assert $ran_block
end
diff --git a/railties/test/secrets_test.rb b/railties/test/secrets_test.rb
index 888fee173a..06877bc76a 100644
--- a/railties/test/secrets_test.rb
+++ b/railties/test/secrets_test.rb
@@ -1,20 +1,13 @@
# frozen_string_literal: true
require "isolation/abstract_unit"
-require "rails/generators"
-require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator"
require "rails/secrets"
class Rails::SecretsTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
- def setup
- build_app
- end
-
- def teardown
- teardown_app
- end
+ setup :build_app
+ teardown :teardown_app
test "setting read to false skips parsing" do
run_secrets_generator do
@@ -172,9 +165,8 @@ class Rails::SecretsTest < ActiveSupport::TestCase
private
def run_secrets_generator
Dir.chdir(app_path) do
- capture(:stdout) do
- Rails::Generators::EncryptedSecretsGenerator.start
- end
+ File.write("config/secrets.yml.key", "f731758c639da2604dfb6bf3d1025de8")
+ File.write("config/secrets.yml.enc", "sEB0mHxDbeP1/KdnMk00wyzPFACl9K6t0cZWn5/Mfx/YbTHvnI07vrneqHg9kaH3wOS7L6pIQteu1P077OtE4BSx/ZRc/sgQPHyWu/tXsrfHqnPNpayOF/XZqizE91JacSFItNMWpuPsp9ynbzz+7cGhoB1S4aPNIU6u0doMrzdngDbijsaAFJmsHIQh6t/QHoJx--8aMoE0PvUWmw1Iqz--ldFqnM/K0g9k17M8PKoN/Q==")
add_to_config <<-RUBY
config.read_encrypted_secrets = true
diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb
index ad852d0f35..91cb47779b 100644
--- a/railties/test/test_unit/reporter_test.rb
+++ b/railties/test/test_unit/reporter_test.rb
@@ -163,7 +163,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
end
def failed_test
- ft = ExampleTest.new(:woot)
+ ft = Minitest::Result.from(ExampleTest.new(:woot))
ft.failures << begin
raise Minitest::Assertion, "boo"
rescue Minitest::Assertion => e
@@ -176,17 +176,17 @@ class TestUnitReporterTest < ActiveSupport::TestCase
error = ArgumentError.new("wups")
error.set_backtrace([ "some_test.rb:4" ])
- et = ExampleTest.new(:woot)
+ et = Minitest::Result.from(ExampleTest.new(:woot))
et.failures << Minitest::UnexpectedError.new(error)
et
end
def passing_test
- ExampleTest.new(:woot)
+ Minitest::Result.from(ExampleTest.new(:woot))
end
def skipped_test
- st = ExampleTest.new(:woot)
+ st = Minitest::Result.from(ExampleTest.new(:woot))
st.failures << begin
raise Minitest::Skip, "skipchurches, misstemples"
rescue Minitest::Assertion => e
diff --git a/tasks/release.rb b/tasks/release.rb
index aa8ba44c1a..e326021dad 100644
--- a/tasks/release.rb
+++ b/tasks/release.rb
@@ -6,7 +6,6 @@ FRAMEWORK_NAMES = Hash.new { |h, k| k.split(/(?<=active|action)/).map(&:capitali
root = File.expand_path("..", __dir__)
version = File.read("#{root}/RAILS_VERSION").strip
tag = "v#{version}"
-gem_version = Gem::Version.new(version)
directory "pkg"
@@ -248,6 +247,12 @@ task :announce do
require "erb"
template = File.read("../tasks/release_announcement_draft.erb")
- puts ERB.new(template, nil, "<>").result(binding)
+
+ match = ERB.version.match(/\Aerb\.rb \[(?<version>[^ ]+) /)
+ if match && match[:version] >= "2.2.0" # Ruby 2.6+
+ puts ERB.new(template, trim_mode: "<>").result(binding)
+ else
+ puts ERB.new(template, nil, "<>").result(binding)
+ end
end
end
diff --git a/version.rb b/version.rb
index 92b5e0392a..54bfbdd516 100644
--- a/version.rb
+++ b/version.rb
@@ -7,8 +7,8 @@ module Rails
end
module VERSION
- MAJOR = 5
- MINOR = 2
+ MAJOR = 6
+ MINOR = 0
TINY = 0
PRE = "alpha"